string
@@ -872,6 +913,13 @@ would select 0.
+
+
+(Appears on:
+ImagePolicySpec)
+
+ReflectionPolicy describes a policy for if/when to reflect a value from the registry in a certain resource field.
diff --git a/docs/spec/v1beta2/imagepolicies.md b/docs/spec/v1beta2/imagepolicies.md
index 8b73621f..989dbf35 100644
--- a/docs/spec/v1beta2/imagepolicies.md
+++ b/docs/spec/v1beta2/imagepolicies.md
@@ -38,7 +38,8 @@ In the above example:
are then used to select the latest tag based on the policy defined in
`.spec.policy`.
- The latest image is constructed with the ImageRepository image and the
- selected tag, and reported in the `.status.latestImage`.
+ selected tag, and reported in the `.status.latestImage` field.
+- The selected tag's digest is reported in the `.status.latestDigest` field.
This example can be run by saving the manifest into `imagepolicy.yaml`.
@@ -67,6 +68,7 @@ Status:
Reason: Succeeded
Status: True
Type: Ready
+ Latest Digest: sha256:2d9a00b3981628a533ff43352193b1838b0a4bf6b0033444286f563205e51a2c
Latest Image: ghcr.io/stefanprodan/podinfo:5.1.4
Observed Generation: 1
Events:
@@ -250,6 +252,19 @@ spec:
In the above example, the timestamp value from the tag pattern is extracted and
used in the policy rule to determine the latest tag.
+### Digest Reflection
+
+`.spec.digestReflectionPolicy` is an optional field that governs the reflection of the selected image's
+digest in the ImagePolicy's `.status.latestDigest` field. The field has three possible values:
+
+- `null`: If the field is set to `null` (or not set at all) the digest will not be reflected at all.
+- `Always`: This value leads to the digest of the latest tag to always be reflected in `.status.
+ latestDigest`. An existing, potentially different digest will be overwritten with the most recent value
+ retrieved from the image registry even if the tag didn't change. This may be useful to track mutable tags
+ like `latest`.
+- `IfNotPresent`: This value will only store the digest of the latest tag once and never overwrite an
+ existing value unless the tag has changed as well. This is the safest option to track immutable tags.
+
## Working with ImagePolicy
### Triggering a reconcile
@@ -333,7 +348,7 @@ specific ImagePolicy, e.g.
### Latest Image
-The ImagePolicy reports the latest select image from the ImageRepository tags in
+The ImagePolicy reports the latest selected image from the ImageRepository tags in
`.status.latestImage` for the resource.
Example:
@@ -346,6 +361,28 @@ metadata:
name:
status:
latestImage: ghcr.io/stefanprodan/podinfo:5.1.4
+[...]
+```
+
+### Latest Digest
+
+Depending on the [chosen digest reflection policy](#digest-reflection) the
+ImagePolicy may report the digest value of the latest selected image from the
+ImageRepository tags in `.status.latestDigest` for the resource. Image digests
+are an immutable reference to a certain image and allow for a stricter policy to
+be applied in comparison to tags which are mutable.
+
+Example:
+
+```yaml
+---
+apiVersion: image.toolkit.fluxcd.io/v1beta2
+kind: ImagePolicy
+metadata:
+ name:
+status:
+ latestDigest: sha256:2d9a00b3981628a533ff43352193b1838b0a4bf6b0033444286f563205e51a2c
+[...]
```
### Observed Previous Image
diff --git a/internal/controller/imagepolicy_controller.go b/internal/controller/imagepolicy_controller.go
index b5fc9103..2f701e3a 100644
--- a/internal/controller/imagepolicy_controller.go
+++ b/internal/controller/imagepolicy_controller.go
@@ -20,9 +20,11 @@ import (
"context"
"errors"
"fmt"
+ "strings"
"time"
"github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
@@ -48,6 +50,7 @@ import (
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
"github.com/fluxcd/image-reflector-controller/internal/policy"
+ "github.com/fluxcd/image-reflector-controller/internal/registry"
)
// errAccessDenied is returned when an ImageRepository reference in ImagePolicy
@@ -109,6 +112,7 @@ type ImagePolicyReconciler struct {
ControllerName string
Database DatabaseReader
ACLOptions acl.Options
+ RegistryHelper registry.Helper
patchOptions []patch.Option
}
@@ -213,9 +217,7 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP
var resultImage, resultTag, previousTag string
- // If there's no error and no requeue is requested, it's a success. Unlike
- // other reconcilers, this reconciler doesn't requeue on its own with a
- // RequeueAfter value.
+ // If there's no error and no requeue is requested, it's a success.
isSuccess := func(res ctrl.Result, err error) bool {
if err != nil || res.Requeue {
return false
@@ -324,6 +326,12 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP
if oldObj.Status.LatestImage != obj.Status.LatestImage {
obj.Status.ObservedPreviousImage = oldObj.Status.LatestImage
}
+
+ if err := r.updateDigest(ctx, repo, obj, latest); err != nil {
+ result, retErr = ctrl.Result{}, err
+ return
+ }
+
// Parse the observed previous image if any and extract previous tag. This
// is used to determine image tag update path.
if obj.Status.ObservedPreviousImage != "" {
@@ -345,6 +353,44 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP
return
}
+func (r *ImagePolicyReconciler) updateDigest(ctx context.Context, repo *imagev1.ImageRepository, obj *imagev1.ImagePolicy, tag string) error {
+ if obj.Spec.DigestReflectionPolicy == nil {
+ obj.Status.LatestDigest = ""
+ return nil
+ }
+
+ if *obj.Spec.DigestReflectionPolicy == imagev1.ReflectIfNotPresent &&
+ obj.Status.LatestDigest != "" &&
+ (obj.Status.ObservedPreviousImage == "" || obj.Status.ObservedPreviousImage == obj.Status.LatestImage) {
+ return nil
+ }
+
+ var err error
+ obj.Status.LatestDigest, err = r.fetchDigest(ctx, repo, tag, obj)
+ if err != nil {
+ return fmt.Errorf("failed fetching digest of %s: %w", obj.Status.LatestImage, err)
+ }
+
+ return nil
+}
+
+func (r *ImagePolicyReconciler) fetchDigest(ctx context.Context, repo *imagev1.ImageRepository, latest string, obj *imagev1.ImagePolicy) (string, error) {
+ ref := strings.Join([]string{repo.Spec.Image, latest}, ":")
+ tagRef, err := name.ParseReference(ref)
+ if err != nil {
+ return "", fmt.Errorf("failed parsing reference %q: %w", ref, err)
+ }
+ opts, err := r.RegistryHelper.GetAuthOptions(ctx, *repo)
+ if err != nil {
+ return "", fmt.Errorf("failed to configure authentication options: %w", err)
+ }
+ desc, err := remote.Head(tagRef, opts...)
+ if err != nil {
+ return "", fmt.Errorf("failed fetching descriptor for %q: %w", tagRef.String(), err)
+ }
+ return desc.Digest.String(), nil
+}
+
// getImageRepository tries to fetch an ImageRepository referenced by the given
// ImagePolicy if it's accessible.
func (r *ImagePolicyReconciler) getImageRepository(ctx context.Context, obj *imagev1.ImagePolicy) (*imagev1.ImageRepository, error) {
diff --git a/internal/controller/imagepolicy_controller_test.go b/internal/controller/imagepolicy_controller_test.go
index c6ec6c40..618d75c0 100644
--- a/internal/controller/imagepolicy_controller_test.go
+++ b/internal/controller/imagepolicy_controller_test.go
@@ -23,10 +23,15 @@ import (
aclapis "github.com/fluxcd/pkg/apis/acl"
"github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/oci/auth/login"
"github.com/fluxcd/pkg/runtime/acl"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -34,6 +39,8 @@ import (
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
"github.com/fluxcd/image-reflector-controller/internal/policy"
+ "github.com/fluxcd/image-reflector-controller/internal/registry"
+ "github.com/fluxcd/image-reflector-controller/internal/test"
)
func TestImagePolicyReconciler_deleteBeforeFinalizer(t *testing.T) {
@@ -262,6 +269,260 @@ func TestImagePolicyReconciler_getImageRepository(t *testing.T) {
}
}
+func TestImagePolicyReconciler_digestReflection(t *testing.T) {
+ polAlways := imagev1.ReflectAlways
+ polIfNotPresent := imagev1.ReflectIfNotPresent
+
+ registryServer := test.NewRegistryServer()
+ defer registryServer.Close()
+
+ versions := []string{"v1.0.0", "v1.1.0", "v1.1.1", "v2.0.0"}
+ imgRepo, images1stPass, err := test.LoadImages(registryServer, "foo/bar", versions)
+ if err != nil {
+ t.Fatalf("could not load images into test registry: %s", err)
+ }
+
+ var images2ndPass map[string]v1.Hash
+
+ tests := []struct {
+ name string
+ semVerPolicy2ndPass string
+ refPolicy1stPass *imagev1.ReflectionPolicy
+ refPolicy2ndPass *imagev1.ReflectionPolicy
+ digest1stPass func() string
+ digest2ndPass func() string
+ }{
+ {
+ name: "nil/missing policy leaves digest empty",
+ refPolicy1stPass: nil,
+ digest1stPass: func() string {
+ return ""
+ },
+ digest2ndPass: func() string {
+ return ""
+ },
+ },
+ {
+ name: "'Always' policy always updates digest",
+ refPolicy1stPass: &polAlways,
+ refPolicy2ndPass: &polAlways,
+ digest1stPass: func() string {
+ return images1stPass["v1.1.1"].String()
+ },
+ digest2ndPass: func() string {
+ return images2ndPass["v1.1.1"].String()
+ },
+ },
+ {
+ name: "'IfNotPresent' policy updates digest when new tag is selected",
+ semVerPolicy2ndPass: "v2.x",
+ refPolicy1stPass: &polIfNotPresent,
+ refPolicy2ndPass: &polIfNotPresent,
+ digest1stPass: func() string {
+ return images1stPass["v1.1.1"].String()
+ },
+ digest2ndPass: func() string {
+ return images2ndPass["v2.0.0"].String()
+ },
+ },
+ {
+ name: "'IfNotPresent' policy only sets digest once",
+ refPolicy1stPass: &polIfNotPresent,
+ refPolicy2ndPass: &polIfNotPresent,
+ digest1stPass: func() string {
+ return images1stPass["v1.1.1"].String()
+ },
+ digest2ndPass: func() string {
+ return images1stPass["v1.1.1"].String()
+ },
+ },
+ {
+ name: "unsetting 'Always' policy removes digest",
+ refPolicy1stPass: &polAlways,
+ refPolicy2ndPass: nil,
+ digest1stPass: func() string {
+ return images1stPass["v1.1.1"].String()
+ },
+ digest2ndPass: func() string {
+ return ""
+ },
+ },
+ {
+ name: "unsetting 'IfNotPresent' policy removes digest",
+ refPolicy1stPass: &polIfNotPresent,
+ refPolicy2ndPass: nil,
+ digest1stPass: func() string {
+ return images1stPass["v1.1.1"].String()
+ },
+ digest2ndPass: func() string {
+ return ""
+ },
+ },
+ {
+ name: "changing 'IfNotPresent' to 'Always' sets new digest",
+ refPolicy1stPass: &polIfNotPresent,
+ refPolicy2ndPass: &polAlways,
+ digest1stPass: func() string {
+ return images1stPass["v1.1.1"].String()
+ },
+ digest2ndPass: func() string {
+ return images2ndPass["v1.1.1"].String()
+ },
+ },
+ {
+ name: "changing 'Always' to 'IfNotPresent' leaves digest untouched",
+ refPolicy1stPass: &polAlways,
+ refPolicy2ndPass: &polIfNotPresent,
+ digest1stPass: func() string {
+ return images1stPass["v1.1.1"].String()
+ },
+ digest2ndPass: func() string {
+ return images1stPass["v1.1.1"].String()
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+
+ g := NewWithT(t)
+
+ // Create namespace where ImagePolicy exists.
+ ns := &corev1.Namespace{}
+ ns.Name = "digref-test"
+
+ // Create ImageRepository.
+ imageRepo := &imagev1.ImageRepository{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: ns.Name,
+ Name: "digref-test",
+ },
+ Spec: imagev1.ImageRepositorySpec{
+ Image: imgRepo,
+ },
+ Status: imagev1.ImageRepositoryStatus{
+ LastScanResult: &imagev1.ScanResult{
+ TagCount: len(versions),
+ LatestTags: versions,
+ },
+ },
+ }
+
+ imagePol := &imagev1.ImagePolicy{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: ns.Name,
+ Name: "digref-test",
+ Finalizers: []string{imagev1.ImagePolicyFinalizer},
+ },
+ Spec: imagev1.ImagePolicySpec{
+ ImageRepositoryRef: meta.NamespacedObjectReference{
+ Name: imageRepo.Name,
+ },
+ DigestReflectionPolicy: tt.refPolicy1stPass,
+ Policy: imagev1.ImagePolicyChoice{
+ SemVer: &imagev1.SemVerPolicy{
+ Range: "v1.x",
+ },
+ },
+ },
+ }
+
+ s := runtime.NewScheme()
+ utilruntime.Must(imagev1.AddToScheme(s))
+ utilruntime.Must(corev1.AddToScheme(s))
+
+ c := fake.NewClientBuilder().
+ WithScheme(s).
+ WithObjects(ns, imageRepo, imagePol).
+ WithStatusSubresource(imagePol).
+ Build()
+
+ g.Expect(
+ c.Get(context.Background(), client.ObjectKeyFromObject(imageRepo), imageRepo),
+ ).To(Succeed(), "failed getting image repo")
+
+ r := &ImagePolicyReconciler{
+ EventRecorder: record.NewFakeRecorder(32),
+ Client: c,
+ Database: &mockDatabase{TagData: imageRepo.Status.LastScanResult.LatestTags},
+ RegistryHelper: registry.NewDefaultHelper(c, login.ProviderOptions{
+ AwsAutoLogin: false,
+ AzureAutoLogin: false,
+ GcpAutoLogin: false,
+ }),
+ }
+
+ res, err := r.Reconcile(context.Background(), ctrl.Request{
+ NamespacedName: types.NamespacedName{
+ Namespace: ns.Name,
+ Name: imagePol.Name,
+ },
+ })
+
+ g.Expect(err).NotTo(HaveOccurred(), "reconciliation failed")
+ g.Expect(res).To(Equal(ctrl.Result{}))
+
+ g.Expect(
+ c.Get(context.Background(), client.ObjectKeyFromObject(imagePol), imagePol),
+ ).To(Succeed(), "failed getting image policy")
+
+ g.Expect(imagePol.Status.LatestDigest).
+ To(Equal(tt.digest1stPass()), "unexpected 1st pass digest in status")
+
+ // Now, change the policy (if the test desires it) and overwrite the existing latest tag with a new image
+
+ defer func() {
+ g.Expect(
+ c.Update(context.Background(), imagePol),
+ ).To(Succeed(), "failed resetting image policy to original values")
+ }()
+
+ if tt.refPolicy1stPass != tt.refPolicy2ndPass {
+ defer func(p *imagev1.ReflectionPolicy) {
+ imagePol.Spec.DigestReflectionPolicy = p
+ }(imagePol.Spec.DigestReflectionPolicy)
+ imagePol.Spec.DigestReflectionPolicy = tt.refPolicy2ndPass
+ }
+ if tt.semVerPolicy2ndPass != "" {
+ defer func(s string) {
+ imagePol.Spec.Policy.SemVer.Range = s
+ }(imagePol.Spec.Policy.SemVer.Range)
+ imagePol.Spec.Policy.SemVer.Range = tt.semVerPolicy2ndPass
+ }
+
+ g.Expect(
+ c.Update(context.Background(), imagePol),
+ ).To(Succeed(), "failed updating image policy for 2nd pass")
+
+ if _, images2ndPass, err = test.LoadImages(registryServer, "foo/bar", versions); err != nil {
+ t.Fatalf("could not overwrite tag: %s", err)
+ }
+
+ defer func() {
+ // the new 1st pass is the old 2nd pass in the next sub-test
+ images1stPass = images2ndPass
+ }()
+
+ res, err = r.Reconcile(context.Background(), ctrl.Request{
+ NamespacedName: types.NamespacedName{
+ Namespace: ns.Name,
+ Name: imagePol.Name,
+ },
+ })
+
+ g.Expect(err).NotTo(HaveOccurred(), "reconciliation failed")
+ g.Expect(res).To(Equal(ctrl.Result{}))
+
+ g.Expect(
+ c.Get(context.Background(), client.ObjectKeyFromObject(imagePol), imagePol),
+ ).To(Succeed(), "failed getting image policy")
+
+ g.Expect(imagePol.Status.LatestDigest).
+ To(Equal(tt.digest2ndPass()), "unexpected 2nd pass digest in status")
+ })
+ }
+}
+
func TestImagePolicyReconciler_applyPolicy(t *testing.T) {
tests := []struct {
name string
diff --git a/internal/controller/imagerepository_controller.go b/internal/controller/imagerepository_controller.go
index 0ae58872..f1156a34 100644
--- a/internal/controller/imagerepository_controller.go
+++ b/internal/controller/imagerepository_controller.go
@@ -22,18 +22,14 @@ import (
"fmt"
"regexp"
"sort"
- "strings"
"time"
- "github.com/google/go-containerregistry/pkg/authn"
- "github.com/google/go-containerregistry/pkg/authn/k8schain"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/types"
kerrors "k8s.io/apimachinery/pkg/util/errors"
kuberecorder "k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
@@ -45,8 +41,6 @@ import (
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta"
- "github.com/fluxcd/pkg/oci"
- "github.com/fluxcd/pkg/oci/auth/login"
"github.com/fluxcd/pkg/runtime/conditions"
helper "github.com/fluxcd/pkg/runtime/controller"
"github.com/fluxcd/pkg/runtime/patch"
@@ -54,7 +48,7 @@ import (
"github.com/fluxcd/pkg/runtime/reconcile"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
- "github.com/fluxcd/image-reflector-controller/internal/secret"
+ "github.com/fluxcd/image-reflector-controller/internal/registry"
)
// latestTagsCount is the number of tags to use as latest tags.
@@ -112,7 +106,8 @@ type ImageRepositoryReconciler struct {
DatabaseWriter
DatabaseReader
}
- DeprecatedLoginOpts login.ProviderOptions
+
+ RegistryHelper registry.Helper
patchOptions []patch.Option
}
@@ -249,7 +244,7 @@ func (r *ImageRepositoryReconciler) reconcile(ctx context.Context, sp *patch.Ser
}
// Parse image reference.
- ref, err := parseImageReference(obj.Spec.Image)
+ ref, err := registry.ParseImageReference(obj.Spec.Image)
if err != nil {
conditions.MarkStalled(obj, imagev1.ImageURLInvalidReason, err.Error())
result, retErr = ctrl.Result{}, nil
@@ -257,7 +252,7 @@ func (r *ImageRepositoryReconciler) reconcile(ctx context.Context, sp *patch.Ser
}
conditions.Delete(obj, meta.StalledCondition)
- opts, err := r.setAuthOptions(ctx, obj, ref)
+ opts, err := r.RegistryHelper.GetAuthOptions(ctx, *obj)
if err != nil {
e := fmt.Errorf("failed to configure authentication options: %w", err)
conditions.MarkFalse(obj, meta.ReadyCondition, imagev1.AuthenticationFailedReason, e.Error())
@@ -323,117 +318,6 @@ func (r *ImageRepositoryReconciler) reconcile(ctx context.Context, sp *patch.Ser
return
}
-// setAuthOptions returns authentication options required to scan a repository.
-func (r *ImageRepositoryReconciler) setAuthOptions(ctx context.Context, obj *imagev1.ImageRepository, ref name.Reference) ([]remote.Option, error) {
- timeout := obj.GetTimeout()
- ctx, cancel := context.WithTimeout(ctx, timeout)
- defer cancel()
-
- // Configure authentication strategy to access the registry.
- var options []remote.Option
- var authSecret corev1.Secret
- var auth authn.Authenticator
- var authErr error
-
- if obj.Spec.SecretRef != nil {
- if err := r.Get(ctx, types.NamespacedName{
- Namespace: obj.GetNamespace(),
- Name: obj.Spec.SecretRef.Name,
- }, &authSecret); err != nil {
- return nil, err
- }
- auth, authErr = secret.AuthFromSecret(authSecret, ref)
- } else {
- // Build login provider options and use it to attempt registry login.
- opts := login.ProviderOptions{}
- switch obj.GetProvider() {
- case "aws":
- opts.AwsAutoLogin = true
- case "azure":
- opts.AzureAutoLogin = true
- case "gcp":
- opts.GcpAutoLogin = true
- default:
- opts = r.DeprecatedLoginOpts
- }
- auth, authErr = login.NewManager().Login(ctx, obj.Spec.Image, ref, opts)
- }
- if authErr != nil {
- // If it's not unconfigured provider error, abort reconciliation.
- // Continue reconciliation if it's unconfigured providers for scanning
- // public repositories.
- if !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
- return nil, authErr
- }
- }
- if auth != nil {
- options = append(options, remote.WithAuth(auth))
- }
-
- // Load any provided certificate.
- if obj.Spec.CertSecretRef != nil {
- var certSecret corev1.Secret
- if obj.Spec.SecretRef != nil && obj.Spec.SecretRef.Name == obj.Spec.CertSecretRef.Name {
- certSecret = authSecret
- } else {
- if err := r.Get(ctx, types.NamespacedName{
- Namespace: obj.GetNamespace(),
- Name: obj.Spec.CertSecretRef.Name,
- }, &certSecret); err != nil {
- return nil, err
- }
- }
-
- tr, err := secret.TransportFromKubeTLSSecret(&certSecret)
- if err != nil {
- return nil, err
- }
- if tr.TLSClientConfig == nil {
- tr, err = secret.TransportFromSecret(&certSecret)
- if err != nil {
- return nil, err
- }
- if tr.TLSClientConfig != nil {
- ctrl.LoggerFrom(ctx).
- Info("warning: specifying TLS auth data via `certFile`/`keyFile`/`caFile` is deprecated, please use `tls.crt`/`tls.key`/`ca.crt` instead")
- }
- }
- options = append(options, remote.WithTransport(tr))
- }
-
- if obj.Spec.ServiceAccountName != "" {
- serviceAccount := corev1.ServiceAccount{}
- // Lookup service account
- if err := r.Get(ctx, types.NamespacedName{
- Namespace: obj.GetNamespace(),
- Name: obj.Spec.ServiceAccountName,
- }, &serviceAccount); err != nil {
- return nil, err
- }
-
- if len(serviceAccount.ImagePullSecrets) > 0 {
- imagePullSecrets := make([]corev1.Secret, len(serviceAccount.ImagePullSecrets))
- for i, ips := range serviceAccount.ImagePullSecrets {
- var saAuthSecret corev1.Secret
- if err := r.Get(ctx, types.NamespacedName{
- Namespace: obj.GetNamespace(),
- Name: ips.Name,
- }, &saAuthSecret); err != nil {
- return nil, err
- }
- imagePullSecrets[i] = saAuthSecret
- }
- keychain, err := k8schain.NewFromPullSecrets(ctx, imagePullSecrets)
- if err != nil {
- return nil, err
- }
- options = append(options, remote.WithAuthFromKeychain(keychain))
- }
- }
-
- return options, nil
-}
-
// shouldScan takes an image repo and the time now, and returns whether
// the repository should be scanned now, and how long to wait for the
// next scan. It also returns the reason for the scan.
@@ -468,7 +352,7 @@ func (r *ImageRepositoryReconciler) shouldScan(obj imagev1.ImageRepository, now
// If the canonical image name of the image is different from the last
// observed name, scan now.
- ref, err := parseImageReference(obj.Spec.Image)
+ ref, err := registry.ParseImageReference(obj.Spec.Image)
if err != nil {
return false, scanInterval, "", err
}
@@ -569,26 +453,6 @@ func eventLogf(ctx context.Context, r kuberecorder.EventRecorder, obj runtime.Ob
r.Eventf(obj, eventType, reason, msg)
}
-// parseImageReference parses the given URL into a container registry repository
-// reference.
-func parseImageReference(url string) (name.Reference, error) {
- if s := strings.Split(url, "://"); len(s) > 1 {
- return nil, fmt.Errorf(".spec.image value should not start with URL scheme; remove '%s://'", s[0])
- }
-
- ref, err := name.ParseReference(url)
- if err != nil {
- return nil, err
- }
-
- imageName := strings.TrimPrefix(url, ref.Context().RegistryStr())
- if s := strings.Split(imageName, ":"); len(s) > 1 {
- return nil, fmt.Errorf(".spec.image value should not contain a tag; remove ':%s'", s[1])
- }
-
- return ref, nil
-}
-
// filterOutTags filters the given tags through the given regular expression
// patterns and returns a list of tags that don't match with the pattern.
func filterOutTags(tags []string, patterns []string) ([]string, error) {
diff --git a/internal/controller/imagerepository_controller_test.go b/internal/controller/imagerepository_controller_test.go
index df25ac43..b14efe15 100644
--- a/internal/controller/imagerepository_controller_test.go
+++ b/internal/controller/imagerepository_controller_test.go
@@ -22,7 +22,6 @@ import (
"testing"
"time"
- "github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
@@ -30,13 +29,12 @@ import (
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
- "github.com/fluxcd/image-reflector-controller/internal/secret"
+ "github.com/fluxcd/image-reflector-controller/internal/registry"
"github.com/fluxcd/image-reflector-controller/internal/test"
)
@@ -98,231 +96,6 @@ func TestImageRepositoryReconciler_deleteBeforeFinalizer(t *testing.T) {
g.Expect(err).NotTo(HaveOccurred())
}
-func TestImageRepositoryReconciler_setAuthOptions(t *testing.T) {
- testImg := "example.com/foo/bar"
- testSecretName := "test-secret"
- testTLSSecretName := "test-tls-secret"
- testDeprecatedTLSSecretName := "test-deprecated-tls-secret"
- testServiceAccountName := "test-service-account"
- testNamespace := "test-ns"
-
- dockerconfigjson := []byte(`
-{
- "auths": {
- "example.com": {
- "username": "user",
- "password": "pass"
- }
- }
-}`)
-
- testSecret := &corev1.Secret{}
- testSecret.Name = testSecretName
- testSecret.Namespace = testNamespace
- testSecret.Type = corev1.SecretTypeDockerConfigJson
- testSecret.Data = map[string][]byte{".dockerconfigjson": dockerconfigjson}
- g := NewWithT(t)
-
- // Create a test TLS server to get valid cert data. The server is never
- // started or used below.
- _, rootCertPEM, clientCertPEM, clientKeyPEM, _, err := test.CreateTLSServer()
- g.Expect(err).To(Not(HaveOccurred()))
-
- testTLSSecret := &corev1.Secret{}
- testTLSSecret.Name = testTLSSecretName
- testTLSSecret.Namespace = testNamespace
- testTLSSecret.Type = corev1.SecretTypeTLS
- testTLSSecret.Data = map[string][]byte{
- secret.CACrtKey: rootCertPEM,
- corev1.TLSCertKey: clientCertPEM,
- corev1.TLSPrivateKeyKey: clientKeyPEM,
- }
-
- testDeprecatedTLSSecret := &corev1.Secret{}
- testDeprecatedTLSSecret.Name = testDeprecatedTLSSecretName
- testDeprecatedTLSSecret.Namespace = testNamespace
- testDeprecatedTLSSecret.Type = corev1.SecretTypeTLS
- testDeprecatedTLSSecret.Data = map[string][]byte{
- secret.CACert: rootCertPEM,
- secret.ClientCert: clientCertPEM,
- secret.ClientKey: clientKeyPEM,
- }
-
- // Docker config secret with TLS data.
- testDockerCfgSecretWithTLS := testSecret.DeepCopy()
- testDockerCfgSecretWithTLS.Data = map[string][]byte{
- secret.CACrtKey: rootCertPEM,
- corev1.TLSCertKey: clientCertPEM,
- corev1.TLSPrivateKeyKey: clientKeyPEM,
- }
-
- // ServiceAccount without image pull secret.
- testServiceAccount := &corev1.ServiceAccount{}
- testServiceAccount.Name = testServiceAccountName
- testServiceAccount.Namespace = testNamespace
-
- // ServiceAccount with image pull secret.
- testServiceAccountWithSecret := testServiceAccount.DeepCopy()
- testServiceAccountWithSecret.ImagePullSecrets = []corev1.LocalObjectReference{{Name: testSecretName}}
-
- tests := []struct {
- name string
- mockObjs []client.Object
- imageRepoSpec imagev1.ImageRepositorySpec
- wantErr bool
- }{
- {
- name: "no auth options",
- imageRepoSpec: imagev1.ImageRepositorySpec{
- Image: testImg,
- },
- },
- {
- name: "secret ref with existing secret",
- mockObjs: []client.Object{testSecret},
- imageRepoSpec: imagev1.ImageRepositorySpec{
- Image: testImg,
- SecretRef: &meta.LocalObjectReference{
- Name: testSecretName,
- },
- },
- },
- {
- name: "secret ref with non-existing secret",
- imageRepoSpec: imagev1.ImageRepositorySpec{
- Image: testImg,
- SecretRef: &meta.LocalObjectReference{
- Name: "non-existing-secret",
- },
- },
- wantErr: true,
- },
- {
- name: "contextual login",
- imageRepoSpec: imagev1.ImageRepositorySpec{
- Image: "123456789000.dkr.ecr.us-east-2.amazonaws.com/test",
- Provider: "aws",
- },
- wantErr: true,
- },
- {
- name: "cloud provider repo without login",
- imageRepoSpec: imagev1.ImageRepositorySpec{
- Image: "123456789000.dkr.ecr.us-east-2.amazonaws.com/test",
- },
- },
- {
- name: "cert secret ref with existing secret",
- mockObjs: []client.Object{testTLSSecret},
- imageRepoSpec: imagev1.ImageRepositorySpec{
- Image: testImg,
- CertSecretRef: &meta.LocalObjectReference{
- Name: testTLSSecretName,
- },
- },
- },
- {
- name: "cert secret ref with existing secret using deprecated keys",
- mockObjs: []client.Object{testDeprecatedTLSSecret},
- imageRepoSpec: imagev1.ImageRepositorySpec{
- Image: testImg,
- CertSecretRef: &meta.LocalObjectReference{
- Name: testDeprecatedTLSSecretName,
- },
- },
- },
- {
- name: "cert secret ref with non-existing secret",
- imageRepoSpec: imagev1.ImageRepositorySpec{
- Image: testImg,
- CertSecretRef: &meta.LocalObjectReference{
- Name: "non-existing-secret",
- },
- },
- wantErr: true,
- },
- {
- name: "secret ref and cert secret ref",
- mockObjs: []client.Object{testSecret, testTLSSecret},
- imageRepoSpec: imagev1.ImageRepositorySpec{
- Image: testImg,
- SecretRef: &meta.LocalObjectReference{
- Name: testSecretName,
- },
- CertSecretRef: &meta.LocalObjectReference{
- Name: testTLSSecretName,
- },
- },
- },
- {
- name: "cert secret ref of type docker config",
- mockObjs: []client.Object{testDockerCfgSecretWithTLS},
- imageRepoSpec: imagev1.ImageRepositorySpec{
- Image: testImg,
- CertSecretRef: &meta.LocalObjectReference{
- Name: testSecretName,
- },
- },
- wantErr: true,
- },
- {
- name: "service account without pull secret",
- mockObjs: []client.Object{testServiceAccount},
- imageRepoSpec: imagev1.ImageRepositorySpec{
- Image: testImg,
- ServiceAccountName: testServiceAccountName,
- },
- },
- {
- name: "service account with pull secret",
- mockObjs: []client.Object{testServiceAccountWithSecret, testSecret},
- imageRepoSpec: imagev1.ImageRepositorySpec{
- Image: testImg,
- ServiceAccountName: testServiceAccountName,
- },
- },
- {
- name: "service account with non-existing pull secret",
- mockObjs: []client.Object{testServiceAccountWithSecret},
- imageRepoSpec: imagev1.ImageRepositorySpec{
- Image: testImg,
- ServiceAccountName: testServiceAccountName,
- },
- wantErr: true,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- g := NewWithT(t)
-
- clientBuilder := fake.NewClientBuilder()
- clientBuilder.WithObjects(tt.mockObjs...)
-
- r := &ImageRepositoryReconciler{
- EventRecorder: record.NewFakeRecorder(32),
- Client: clientBuilder.Build(),
- patchOptions: getPatchOptions(imageRepositoryOwnedConditions, "irc"),
- }
-
- obj := &imagev1.ImageRepository{
- ObjectMeta: metav1.ObjectMeta{
- GenerateName: "reconcile-repo-",
- Generation: 1,
- Namespace: testNamespace,
- },
- }
- obj.Spec = tt.imageRepoSpec
-
- ref, err := name.ParseReference(obj.Spec.Image)
- g.Expect(err).ToNot(HaveOccurred())
-
- _, err = r.setAuthOptions(ctx, obj, ref)
- g.Expect(err != nil).To(Equal(tt.wantErr))
- })
- }
-}
-
func TestImageRepositoryReconciler_shouldScan(t *testing.T) {
testImage := "example.com/foo/bar"
tests := []struct {
@@ -561,7 +334,7 @@ func TestImageRepositoryReconciler_scan(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
- imgRepo, err := test.LoadImages(registryServer, "test-fetch-"+randStringRunes(5), tt.tags)
+ imgRepo, _, err := test.LoadImages(registryServer, "test-fetch-"+randStringRunes(5), tt.tags)
g.Expect(err).ToNot(HaveOccurred())
r := ImageRepositoryReconciler{
@@ -580,7 +353,7 @@ func TestImageRepositoryReconciler_scan(t *testing.T) {
repo.SetAnnotations(map[string]string{meta.ReconcileRequestAnnotation: tt.annotation})
}
- ref, err := parseImageReference(imgRepo)
+ ref, err := registry.ParseImageReference(imgRepo)
g.Expect(err).ToNot(HaveOccurred())
opts := []remote.Option{}
@@ -656,49 +429,6 @@ func TestGetLatestTags(t *testing.T) {
}
}
-func TestParseImageReference(t *testing.T) {
- tests := []struct {
- name string
- url string
- wantErr bool
- wantRef string
- }{
- {
- name: "simple valid url",
- url: "example.com/foo/bar",
- wantRef: "example.com/foo/bar",
- },
- {
- name: "with scheme prefix",
- url: "https://example.com/foo/bar",
- wantErr: true,
- },
- {
- name: "with tag",
- url: "example.com/foo/bar:baz",
- wantErr: true,
- },
- {
- name: "with host port",
- url: "example.com:9999/foo/bar",
- wantErr: false,
- wantRef: "example.com:9999/foo/bar",
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- g := NewWithT(t)
-
- ref, err := parseImageReference(tt.url)
- g.Expect(err != nil).To(Equal(tt.wantErr))
- if err == nil {
- g.Expect(ref.String()).To(Equal(tt.wantRef))
- }
- })
- }
-}
-
func TestFilterOutTags(t *testing.T) {
tests := []struct {
name string
diff --git a/internal/controller/policy_test.go b/internal/controller/policy_test.go
index 47cc9611..e442e5f2 100644
--- a/internal/controller/policy_test.go
+++ b/internal/controller/policy_test.go
@@ -51,7 +51,7 @@ func TestImagePolicyReconciler_crossNamespaceRefsDisallowed(t *testing.T) {
defer registryServer.Close()
versions := []string{"1.0.1", "1.0.2", "1.1.0-alpha"}
- imgRepo, err := test.LoadImages(registryServer, "test-semver-policy-"+randStringRunes(5), versions)
+ imgRepo, _, err := test.LoadImages(registryServer, "test-semver-policy-"+randStringRunes(5), versions)
g.Expect(err).ToNot(HaveOccurred())
namespaceLabels := map[string]string{
@@ -171,7 +171,7 @@ func TestImagePolicyReconciler_calculateImageFromRepoTags(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
- imgRepo, err := test.LoadImages(registryServer, "test-semver-policy-"+randStringRunes(5), tt.versions)
+ imgRepo, _, err := test.LoadImages(registryServer, "test-semver-policy-"+randStringRunes(5), tt.versions)
g.Expect(err).ToNot(HaveOccurred())
repo := imagev1.ImageRepository{
@@ -219,7 +219,8 @@ func TestImagePolicyReconciler_calculateImageFromRepoTags(t *testing.T) {
if !tt.wantFailure {
g.Eventually(func() bool {
err := testEnv.Get(ctx, polName, &pol)
- return err == nil && pol.Status.LatestImage != ""
+ return err == nil &&
+ pol.Status.LatestImage != ""
}, timeout, interval).Should(BeTrue())
g.Expect(pol.Status.LatestImage).To(Equal(imgRepo + tt.wantImageTag))
} else {
@@ -276,7 +277,7 @@ func TestImagePolicyReconciler_filterTags(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
- imgRepo, err := test.LoadImages(registryServer, "test-semver-policy-"+randStringRunes(5), tt.versions)
+ imgRepo, _, err := test.LoadImages(registryServer, "test-semver-policy-"+randStringRunes(5), tt.versions)
g.Expect(err).ToNot(HaveOccurred())
repo := imagev1.ImageRepository{
@@ -440,7 +441,7 @@ func TestImagePolicyReconciler_accessImageRepo(t *testing.T) {
g := NewWithT(t)
versions := []string{"1.0.0", "1.0.1"}
- imgRepo, err := test.LoadImages(registryServer, "acl-image-"+randStringRunes(5), versions)
+ imgRepo, _, err := test.LoadImages(registryServer, "acl-image-"+randStringRunes(5), versions)
g.Expect(err).ToNot(HaveOccurred())
ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
diff --git a/internal/controller/scan_test.go b/internal/controller/scan_test.go
index 0ef2f5d4..f7c999a3 100644
--- a/internal/controller/scan_test.go
+++ b/internal/controller/scan_test.go
@@ -116,7 +116,7 @@ func TestImageRepositoryReconciler_fetchImageTags(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- imgRepo, err := test.LoadImages(registryServer, "test-fetch-"+randStringRunes(5), tt.versions)
+ imgRepo, _, err := test.LoadImages(registryServer, "test-fetch-"+randStringRunes(5), tt.versions)
g.Expect(err).ToNot(HaveOccurred())
repo := imagev1.ImageRepository{
@@ -209,7 +209,7 @@ func TestImageRepositoryReconciler_reconcileAtAnnotation(t *testing.T) {
registryServer := test.NewRegistryServer()
defer registryServer.Close()
- imgRepo, err := test.LoadImages(registryServer, "test-annot-"+randStringRunes(5), []string{"1.0.0"})
+ imgRepo, _, err := test.LoadImages(registryServer, "test-annot-"+randStringRunes(5), []string{"1.0.0"})
g.Expect(err).ToNot(HaveOccurred())
repo := imagev1.ImageRepository{
@@ -289,7 +289,7 @@ func TestImageRepositoryReconciler_authRegistry(t *testing.T) {
}()
versions := []string{"0.1.0", "0.1.1", "0.2.0", "1.0.0", "1.0.1", "1.0.2", "1.1.0-alpha"}
- imgRepo, err := test.LoadImages(registryServer, "test-authn-"+randStringRunes(5),
+ imgRepo, _, err := test.LoadImages(registryServer, "test-authn-"+randStringRunes(5),
versions, remote.WithAuth(&authn.Basic{
Username: username,
Password: password,
@@ -339,7 +339,7 @@ func TestImageRepositoryReconciler_imageAttribute_schemePrefix(t *testing.T) {
registryServer := test.NewRegistryServer()
defer registryServer.Close()
- imgRepo, err := test.LoadImages(registryServer, "test-fetch", []string{"1.0.0"})
+ imgRepo, _, err := test.LoadImages(registryServer, "test-fetch", []string{"1.0.0"})
g.Expect(err).ToNot(HaveOccurred())
imgRepo = "https://" + imgRepo
@@ -384,7 +384,7 @@ func TestImageRepositoryReconciler_imageAttribute_withTag(t *testing.T) {
registryServer := test.NewRegistryServer()
defer registryServer.Close()
- imgRepo, err := test.LoadImages(registryServer, "test-fetch", []string{"1.0.0"})
+ imgRepo, _, err := test.LoadImages(registryServer, "test-fetch", []string{"1.0.0"})
g.Expect(err).ToNot(HaveOccurred())
imgRepo = imgRepo + ":1.0.0"
@@ -429,7 +429,7 @@ func TestImageRepositoryReconciler_imageAttribute_hostPort(t *testing.T) {
registryServer := test.NewRegistryServer()
defer registryServer.Close()
- imgRepo, err := test.LoadImages(registryServer, "test-fetch", []string{"1.0.0"})
+ imgRepo, _, err := test.LoadImages(registryServer, "test-fetch", []string{"1.0.0"})
g.Expect(err).ToNot(HaveOccurred())
imgRepo = strings.ReplaceAll(imgRepo, "127.0.0.1", "localhost")
@@ -505,7 +505,7 @@ func TestImageRepositoryReconciler_authRegistryWithServiceAccount(t *testing.T)
}()
versions := []string{"0.1.0", "0.1.1", "0.2.0", "1.0.0", "1.0.1", "1.0.2", "1.1.0-alpha"}
- imgRepo, err := test.LoadImages(registryServer, "test-authn-"+randStringRunes(5),
+ imgRepo, _, err := test.LoadImages(registryServer, "test-authn-"+randStringRunes(5),
versions, remote.WithAuth(&authn.Basic{
Username: username,
Password: password,
diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go
index cbb4b5a7..cf4dfd64 100644
--- a/internal/controller/suite_test.go
+++ b/internal/controller/suite_test.go
@@ -31,11 +31,13 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
+ "github.com/fluxcd/pkg/oci/auth/login"
"github.com/fluxcd/pkg/runtime/controller"
"github.com/fluxcd/pkg/runtime/testenv"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
"github.com/fluxcd/image-reflector-controller/internal/database"
+ "github.com/fluxcd/image-reflector-controller/internal/registry"
// +kubebuilder:scaffold:imports
)
@@ -60,10 +62,6 @@ var (
ctx = ctrl.SetupSignalHandler()
)
-func init() {
- rand.Seed(time.Now().UnixNano())
-}
-
func TestMain(m *testing.M) {
utilruntime.Must(imagev1.AddToScheme(scheme.Scheme))
@@ -89,10 +87,13 @@ func TestMain(m *testing.M) {
panic(fmt.Sprintf("Failed to create new Badger database: %v", err))
}
+ regHelper := registry.NewDefaultHelper(testEnv, login.ProviderOptions{})
+
if err = (&ImageRepositoryReconciler{
- Client: testEnv,
- Database: database.NewBadgerDatabase(testBadgerDB),
- EventRecorder: record.NewFakeRecorder(256),
+ Client: testEnv,
+ Database: database.NewBadgerDatabase(testBadgerDB),
+ EventRecorder: record.NewFakeRecorder(256),
+ RegistryHelper: regHelper,
}).SetupWithManager(testEnv, ImageRepositoryReconcilerOptions{
RateLimiter: controller.GetDefaultRateLimiter(),
}); err != nil {
@@ -100,9 +101,10 @@ func TestMain(m *testing.M) {
}
if err = (&ImagePolicyReconciler{
- Client: testEnv,
- Database: database.NewBadgerDatabase(testBadgerDB),
- EventRecorder: record.NewFakeRecorder(256),
+ Client: testEnv,
+ Database: database.NewBadgerDatabase(testBadgerDB),
+ EventRecorder: record.NewFakeRecorder(256),
+ RegistryHelper: regHelper,
}).SetupWithManager(testEnv, ImagePolicyReconcilerOptions{
RateLimiter: controller.GetDefaultRateLimiter(),
}); err != nil {
diff --git a/internal/registry/helper.go b/internal/registry/helper.go
new file mode 100644
index 00000000..af3b736e
--- /dev/null
+++ b/internal/registry/helper.go
@@ -0,0 +1,51 @@
+package registry
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
+ "github.com/fluxcd/pkg/oci/auth/login"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type Helper interface {
+ GetAuthOptions(ctx context.Context, obj imagev1.ImageRepository) ([]remote.Option, error)
+}
+
+type DefaultHelper struct {
+ k8sClient client.Client
+ DeprecatedLoginOpts login.ProviderOptions
+}
+
+var _ Helper = DefaultHelper{}
+
+func NewDefaultHelper(c client.Client, deprecatedLoginOpts login.ProviderOptions) DefaultHelper {
+ return DefaultHelper{
+ k8sClient: c,
+ DeprecatedLoginOpts: deprecatedLoginOpts,
+ }
+}
+
+// ParseImageReference parses the given URL into a container registry repository
+// reference.
+func ParseImageReference(url string) (name.Reference, error) {
+ if s := strings.Split(url, "://"); len(s) > 1 {
+ return nil, fmt.Errorf(".spec.image value should not start with URL scheme; remove '%s://'", s[0])
+ }
+
+ ref, err := name.ParseReference(url)
+ if err != nil {
+ return nil, err
+ }
+
+ imageName := strings.TrimPrefix(url, ref.Context().RegistryStr())
+ if s := strings.Split(imageName, ":"); len(s) > 1 {
+ return nil, fmt.Errorf(".spec.image value should not contain a tag; remove ':%s'", s[1])
+ }
+
+ return ref, nil
+}
diff --git a/internal/registry/helper_test.go b/internal/registry/helper_test.go
new file mode 100644
index 00000000..b401a77e
--- /dev/null
+++ b/internal/registry/helper_test.go
@@ -0,0 +1,289 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package registry_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/oci/auth/login"
+ . "github.com/onsi/gomega"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+ imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
+ "github.com/fluxcd/image-reflector-controller/internal/registry"
+ "github.com/fluxcd/image-reflector-controller/internal/secret"
+ "github.com/fluxcd/image-reflector-controller/internal/test"
+)
+
+func TestDefaultHelperAuthOptions(t *testing.T) {
+ testImg := "example.com/foo/bar"
+ testSecretName := "test-secret"
+ testTLSSecretName := "test-tls-secret"
+ testDeprecatedTLSSecretName := "test-deprecated-tls-secret"
+ testServiceAccountName := "test-service-account"
+ testNamespace := "test-ns"
+
+ dockerconfigjson := []byte(`
+{
+ "auths": {
+ "example.com": {
+ "username": "user",
+ "password": "pass"
+ }
+ }
+}`)
+
+ testSecret := &corev1.Secret{}
+ testSecret.Name = testSecretName
+ testSecret.Namespace = testNamespace
+ testSecret.Type = corev1.SecretTypeDockerConfigJson
+ testSecret.Data = map[string][]byte{".dockerconfigjson": dockerconfigjson}
+ g := NewWithT(t)
+
+ // Create a test TLS server to get valid cert data. The server is never
+ // started or used below.
+ _, rootCertPEM, clientCertPEM, clientKeyPEM, _, err := test.CreateTLSServer()
+ g.Expect(err).To(Not(HaveOccurred()))
+
+ testTLSSecret := &corev1.Secret{}
+ testTLSSecret.Name = testTLSSecretName
+ testTLSSecret.Namespace = testNamespace
+ testTLSSecret.Type = corev1.SecretTypeTLS
+ testTLSSecret.Data = map[string][]byte{
+ secret.CACert: rootCertPEM,
+ secret.ClientCert: clientCertPEM,
+ secret.ClientKey: clientKeyPEM,
+ }
+
+ testDeprecatedTLSSecret := &corev1.Secret{}
+ testDeprecatedTLSSecret.Name = testDeprecatedTLSSecretName
+ testDeprecatedTLSSecret.Namespace = testNamespace
+ testDeprecatedTLSSecret.Type = corev1.SecretTypeTLS
+ testDeprecatedTLSSecret.Data = map[string][]byte{
+ secret.CACert: rootCertPEM,
+ secret.ClientCert: clientCertPEM,
+ secret.ClientKey: clientKeyPEM,
+ }
+
+ // Secret with docker config and TLS secrets.
+ testDockerCfgSecretWithTLS := testSecret.DeepCopy()
+ testDockerCfgSecretWithTLS.Data = map[string][]byte{
+ secret.CACrtKey: rootCertPEM,
+ corev1.TLSCertKey: clientCertPEM,
+ corev1.TLSPrivateKeyKey: clientKeyPEM,
+ }
+
+ // ServiceAccount without image pull secret.
+ testServiceAccount := &corev1.ServiceAccount{}
+ testServiceAccount.Name = testServiceAccountName
+ testServiceAccount.Namespace = testNamespace
+
+ // ServiceAccount with image pull secret.
+ testServiceAccountWithSecret := testServiceAccount.DeepCopy()
+ testServiceAccountWithSecret.ImagePullSecrets = []corev1.LocalObjectReference{{Name: testSecretName}}
+
+ tests := []struct {
+ name string
+ k8sObjs []client.Object
+ repo imagev1.ImageRepository
+ expectErr bool
+ expectOpts int
+ }{
+ {
+ name: "adds authenticator from secret",
+ repo: imagev1.ImageRepository{
+ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace},
+ Spec: imagev1.ImageRepositorySpec{
+ Image: testImg,
+ SecretRef: &meta.LocalObjectReference{
+ Name: testSecretName,
+ },
+ },
+ },
+ k8sObjs: []client.Object{testSecret},
+ expectErr: false,
+ expectOpts: 1,
+ },
+ {
+ name: "fails with non-existing cert secret ref",
+ repo: imagev1.ImageRepository{
+ Spec: imagev1.ImageRepositorySpec{
+ Image: testImg,
+ CertSecretRef: &meta.LocalObjectReference{
+ Name: "non-existing-secret",
+ },
+ },
+ },
+ expectErr: true,
+ expectOpts: 0,
+ },
+ {
+ name: "sets transport from cert secret ref",
+ repo: imagev1.ImageRepository{
+ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace},
+ Spec: imagev1.ImageRepositorySpec{
+ Image: testImg,
+ CertSecretRef: &meta.LocalObjectReference{
+ Name: testTLSSecretName,
+ },
+ },
+ },
+ k8sObjs: []client.Object{testTLSSecret},
+ expectErr: false,
+ expectOpts: 1,
+ },
+ {
+ name: "sets transport and auth from secret ref and cert secret ref",
+ repo: imagev1.ImageRepository{
+ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace},
+ Spec: imagev1.ImageRepositorySpec{
+ Image: testImg,
+ SecretRef: &meta.LocalObjectReference{
+ Name: testSecretName,
+ },
+ CertSecretRef: &meta.LocalObjectReference{
+ Name: testTLSSecretName,
+ },
+ },
+ },
+ k8sObjs: []client.Object{testSecret, testTLSSecret},
+ expectErr: false,
+ expectOpts: 2,
+ },
+ {
+ name: "sets auth options cert secret ref with existing secret using deprecated keys",
+ k8sObjs: []client.Object{testDeprecatedTLSSecret},
+ repo: imagev1.ImageRepository{
+ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace},
+ Spec: imagev1.ImageRepositorySpec{
+ Image: testImg,
+ CertSecretRef: &meta.LocalObjectReference{
+ Name: testDeprecatedTLSSecretName,
+ },
+ },
+ },
+ expectErr: false,
+ expectOpts: 1,
+ },
+ {
+ name: "fails with cert secret ref of type docker config",
+ repo: imagev1.ImageRepository{
+ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace},
+ Spec: imagev1.ImageRepositorySpec{
+ Image: testImg,
+ CertSecretRef: &meta.LocalObjectReference{
+ Name: testSecretName,
+ },
+ },
+ },
+ k8sObjs: []client.Object{testDockerCfgSecretWithTLS},
+ expectErr: true,
+ },
+ {
+ name: "sets auth option from SA with pull secret",
+ repo: imagev1.ImageRepository{
+ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace},
+ Spec: imagev1.ImageRepositorySpec{
+ Image: testImg,
+ ServiceAccountName: testServiceAccountName,
+ },
+ },
+ k8sObjs: []client.Object{testSecret, testServiceAccountWithSecret},
+ expectErr: false,
+ expectOpts: 1,
+ },
+ {
+ name: "fails with SA an non-existing pull secret",
+ repo: imagev1.ImageRepository{
+ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace},
+ Spec: imagev1.ImageRepositorySpec{
+ Image: testImg,
+ ServiceAccountName: testServiceAccountName,
+ },
+ },
+ k8sObjs: []client.Object{testServiceAccountWithSecret},
+ expectErr: true,
+ expectOpts: 0,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+ k8sClient := fake.NewClientBuilder().
+ WithObjects(tt.k8sObjs...).
+ Build()
+ h := registry.NewDefaultHelper(k8sClient, login.ProviderOptions{})
+
+ opts, err := h.GetAuthOptions(context.Background(), tt.repo)
+ if tt.expectErr {
+ g.Expect(err).To(HaveOccurred())
+ } else {
+ g.Expect(err).NotTo(HaveOccurred())
+ }
+ g.Expect(opts).To(HaveLen(tt.expectOpts))
+ })
+ }
+}
+
+func TestParseImageReference(t *testing.T) {
+ tests := []struct {
+ name string
+ url string
+ wantErr bool
+ wantRef string
+ }{
+ {
+ name: "simple valid url",
+ url: "example.com/foo/bar",
+ wantRef: "example.com/foo/bar",
+ },
+ {
+ name: "with scheme prefix",
+ url: "https://example.com/foo/bar",
+ wantErr: true,
+ },
+ {
+ name: "with tag",
+ url: "example.com/foo/bar:baz",
+ wantErr: true,
+ },
+ {
+ name: "with host port",
+ url: "example.com:9999/foo/bar",
+ wantErr: false,
+ wantRef: "example.com:9999/foo/bar",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ ref, err := registry.ParseImageReference(tt.url)
+ g.Expect(err != nil).To(Equal(tt.wantErr))
+ if err == nil {
+ g.Expect(ref.String()).To(Equal(tt.wantRef))
+ }
+ })
+ }
+}
diff --git a/internal/registry/options.go b/internal/registry/options.go
new file mode 100644
index 00000000..06cdf5a1
--- /dev/null
+++ b/internal/registry/options.go
@@ -0,0 +1,135 @@
+package registry
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/fluxcd/pkg/oci"
+ "github.com/fluxcd/pkg/oci/auth/login"
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/authn/k8schain"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/types"
+ ctrl "sigs.k8s.io/controller-runtime"
+
+ imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
+ "github.com/fluxcd/image-reflector-controller/internal/secret"
+)
+
+// GetAuthOptions returns authentication options required to scan a repository.
+func (h DefaultHelper) GetAuthOptions(ctx context.Context, obj imagev1.ImageRepository) ([]remote.Option, error) {
+ timeout := obj.GetTimeout()
+ ctx, cancel := context.WithTimeout(ctx, timeout)
+ defer cancel()
+
+ // Configure authentication strategy to access the registry.
+ var options []remote.Option
+ var authSecret corev1.Secret
+ var auth authn.Authenticator
+ var authErr error
+
+ ref, err := ParseImageReference(obj.Spec.Image)
+ if err != nil {
+ return nil, fmt.Errorf("failed parsing image reference: %w", err)
+ }
+
+ if obj.Spec.SecretRef != nil {
+ if err := h.k8sClient.Get(ctx, types.NamespacedName{
+ Namespace: obj.GetNamespace(),
+ Name: obj.Spec.SecretRef.Name,
+ }, &authSecret); err != nil {
+ return nil, err
+ }
+ auth, authErr = secret.AuthFromSecret(authSecret, ref)
+ } else {
+ // Build login provider options and use it to attempt registry login.
+ opts := login.ProviderOptions{}
+ switch obj.GetProvider() {
+ case "aws":
+ opts.AwsAutoLogin = true
+ case "azure":
+ opts.AzureAutoLogin = true
+ case "gcp":
+ opts.GcpAutoLogin = true
+ default:
+ opts = h.DeprecatedLoginOpts
+ }
+ auth, authErr = login.NewManager().Login(ctx, obj.Spec.Image, ref, opts)
+ }
+ if authErr != nil {
+ // If it's not unconfigured provider error, abort reconciliation.
+ // Continue reconciliation if it's unconfigured providers for scanning
+ // public repositories.
+ if !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
+ return nil, authErr
+ }
+ }
+ if auth != nil {
+ options = append(options, remote.WithAuth(auth))
+ }
+
+ // Load any provided certificate.
+ if obj.Spec.CertSecretRef != nil {
+ var certSecret corev1.Secret
+ if obj.Spec.SecretRef != nil && obj.Spec.SecretRef.Name == obj.Spec.CertSecretRef.Name {
+ certSecret = authSecret
+ } else {
+ if err := h.k8sClient.Get(ctx, types.NamespacedName{
+ Namespace: obj.GetNamespace(),
+ Name: obj.Spec.CertSecretRef.Name,
+ }, &certSecret); err != nil {
+ return nil, err
+ }
+ }
+
+ tr, err := secret.TransportFromKubeTLSSecret(&certSecret)
+ if err != nil {
+ return nil, err
+ }
+ if tr.TLSClientConfig == nil {
+ tr, err = secret.TransportFromSecret(&certSecret)
+ if err != nil {
+ return nil, err
+ }
+ if tr.TLSClientConfig != nil {
+ ctrl.LoggerFrom(ctx).
+ Info("warning: specifying TLS auth data via `certFile`/`keyFile`/`caFile` is deprecated, please use `tls.crt`/`tls.key`/`ca.crt` instead")
+ }
+ }
+ options = append(options, remote.WithTransport(tr))
+ }
+
+ if obj.Spec.ServiceAccountName != "" {
+ serviceAccount := corev1.ServiceAccount{}
+ // Lookup service account
+ if err := h.k8sClient.Get(ctx, types.NamespacedName{
+ Namespace: obj.GetNamespace(),
+ Name: obj.Spec.ServiceAccountName,
+ }, &serviceAccount); err != nil {
+ return nil, err
+ }
+
+ if len(serviceAccount.ImagePullSecrets) > 0 {
+ imagePullSecrets := make([]corev1.Secret, len(serviceAccount.ImagePullSecrets))
+ for i, ips := range serviceAccount.ImagePullSecrets {
+ var saAuthSecret corev1.Secret
+ if err := h.k8sClient.Get(ctx, types.NamespacedName{
+ Namespace: obj.GetNamespace(),
+ Name: ips.Name,
+ }, &saAuthSecret); err != nil {
+ return nil, err
+ }
+ imagePullSecrets[i] = saAuthSecret
+ }
+ keychain, err := k8schain.NewFromPullSecrets(ctx, imagePullSecrets)
+ if err != nil {
+ return nil, err
+ }
+ options = append(options, remote.WithAuthFromKeychain(keychain))
+ }
+ }
+
+ return options, nil
+}
diff --git a/internal/test/registry.go b/internal/test/registry.go
index c26e7462..c7414f0f 100644
--- a/internal/test/registry.go
+++ b/internal/test/registry.go
@@ -27,6 +27,7 @@ import (
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
@@ -78,22 +79,29 @@ func RegistryName(srv *httptest.Server) string {
// image repo
// name. https://github.com/google/go-containerregistry/blob/v0.1.1/pkg/registry/compatibility_test.go
// has an example of loading a test registry with a random image.
-func LoadImages(srv *httptest.Server, imageName string, versions []string, options ...remote.Option) (string, error) {
+func LoadImages(srv *httptest.Server, imageName string, versions []string, options ...remote.Option) (string, map[string]v1.Hash, error) {
imgRepo := RegistryName(srv) + "/" + imageName
+ imgRes := make(map[string]v1.Hash, 0)
+
for _, tag := range versions {
imgRef, err := name.NewTag(imgRepo + ":" + tag)
if err != nil {
- return imgRepo, err
+ return imgRepo, nil, err
}
img, err := random.Image(512, 1)
if err != nil {
- return imgRepo, err
+ return imgRepo, nil, err
}
if err := remote.Write(imgRef, img, options...); err != nil {
- return imgRepo, err
+ return imgRepo, nil, err
+ }
+ dig, err := img.Digest()
+ if err != nil {
+ return imgRepo, nil, err
}
+ imgRes[tag] = dig
}
- return imgRepo, nil
+ return imgRepo, imgRes, nil
}
// the go-containerregistry test registry implementation does not
diff --git a/internal/test/registry_test.go b/internal/test/registry_test.go
index 7d078e04..7a9fb5df 100644
--- a/internal/test/registry_test.go
+++ b/internal/test/registry_test.go
@@ -32,7 +32,7 @@ func TestRegistryHandler(t *testing.T) {
defer srv.Close()
uploadedTags := []string{"tag1", "tag2"}
- repoString, err := LoadImages(srv, "testimage", uploadedTags)
+ repoString, _, err := LoadImages(srv, "testimage", uploadedTags)
g.Expect(err).ToNot(HaveOccurred())
repo, _ := name.NewRepository(repoString)
diff --git a/main.go b/main.go
index a94fadde..10b834a9 100644
--- a/main.go
+++ b/main.go
@@ -52,6 +52,7 @@ import (
"github.com/fluxcd/image-reflector-controller/internal/controller"
"github.com/fluxcd/image-reflector-controller/internal/database"
"github.com/fluxcd/image-reflector-controller/internal/features"
+ "github.com/fluxcd/image-reflector-controller/internal/registry"
)
const controllerName = "image-reflector-controller"
@@ -205,17 +206,20 @@ func main() {
metricsH := helper.NewMetrics(mgr, metrics.MustMakeRecorder(), imagev1.ImageFinalizer)
+ deprecatedLoginOptions := login.ProviderOptions{
+ AwsAutoLogin: awsAutoLogin,
+ AzureAutoLogin: azureAutoLogin,
+ GcpAutoLogin: gcpAutoLogin,
+ }
+ registryHelper := registry.NewDefaultHelper(mgr.GetClient(), deprecatedLoginOptions)
+
if err := (&controller.ImageRepositoryReconciler{
Client: mgr.GetClient(),
EventRecorder: eventRecorder,
Metrics: metricsH,
Database: db,
ControllerName: controllerName,
- DeprecatedLoginOpts: login.ProviderOptions{
- AwsAutoLogin: awsAutoLogin,
- AzureAutoLogin: azureAutoLogin,
- GcpAutoLogin: gcpAutoLogin,
- },
+ RegistryHelper: registryHelper,
}).SetupWithManager(mgr, controller.ImageRepositoryReconcilerOptions{
RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
}); err != nil {
@@ -229,6 +233,7 @@ func main() {
Database: db,
ACLOptions: aclOptions,
ControllerName: controllerName,
+ RegistryHelper: registryHelper,
}).SetupWithManager(mgr, controller.ImagePolicyReconcilerOptions{
RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
}); err != nil {
From ab8a91ac80c24311b1a5976c39721d1593b2d4c9 Mon Sep 17 00:00:00 2001
From: Max Jonas Werner
Date: Wed, 16 Aug 2023 09:40:19 +0200
Subject: [PATCH 02/17] Introduce ImagePolicy.Status.LatestRef field
This new field summarizes all data reflecting an image reference, i.e.
the repository name, tag and digest.
Since this change changes the API in a backwards-incompatible way, the
new API version v1beta3 is introduced.
Signed-off-by: Max Jonas Werner
---
Makefile | 2 +-
PROJECT | 6 +
api/v1beta1/zz_generated.deepcopy.go | 2 +-
api/v1beta2/imagepolicy_types.go | 21 -
api/v1beta2/imagerepository_types.go | 1 -
api/v1beta2/zz_generated.deepcopy.go | 7 +-
api/v1beta3/condition_types.go | 35 +
api/v1beta3/doc.go | 24 +
api/v1beta3/groupversion_info.go | 36 +
api/v1beta3/imagepolicy_types.go | 189 ++++
api/v1beta3/imagerepository_types.go | 209 +++++
api/v1beta3/zz_generated.deepcopy.go | 402 +++++++++
...image.toolkit.fluxcd.io_imagepolicies.yaml | 231 ++++-
...e.toolkit.fluxcd.io_imagerepositories.yaml | 240 ++++++
config/samples/image_v1beta3_imagepolicy.yaml | 11 +
.../image_v1beta3_imagerepository.yaml | 12 +
.../{v1beta2 => v1beta3}/image-reflector.md | 170 ++--
docs/spec/v1beta3/imagepolicies.md | 495 +++++++++++
docs/spec/v1beta3/imagerepositories.md | 811 ++++++++++++++++++
hack/boilerplate.go.txt | 2 +-
.../controller/controllers_fuzzer_test.go | 2 +-
internal/controller/imagepolicy_controller.go | 33 +-
.../controller/imagepolicy_controller_test.go | 6 +-
.../controller/imagerepository_controller.go | 2 +-
.../imagerepository_controller_test.go | 2 +-
internal/controller/policy_test.go | 14 +-
internal/controller/scan_test.go | 2 +-
internal/controller/suite_test.go | 2 +-
internal/policy/factory.go | 2 +-
internal/policy/factory_test.go | 2 +-
internal/registry/helper.go | 2 +-
internal/registry/helper_test.go | 2 +-
internal/registry/options.go | 2 +-
main.go | 2 +-
tests/integration/imagerepo_test.go | 2 +-
tests/integration/suite_test.go | 2 +-
36 files changed, 2844 insertions(+), 141 deletions(-)
create mode 100644 api/v1beta3/condition_types.go
create mode 100644 api/v1beta3/doc.go
create mode 100644 api/v1beta3/groupversion_info.go
create mode 100644 api/v1beta3/imagepolicy_types.go
create mode 100644 api/v1beta3/imagerepository_types.go
create mode 100644 api/v1beta3/zz_generated.deepcopy.go
create mode 100644 config/samples/image_v1beta3_imagepolicy.yaml
create mode 100644 config/samples/image_v1beta3_imagerepository.yaml
rename docs/api/{v1beta2 => v1beta3}/image-reflector.md (82%)
create mode 100644 docs/spec/v1beta3/imagepolicies.md
create mode 100644 docs/spec/v1beta3/imagerepositories.md
diff --git a/Makefile b/Makefile
index 5db80131..48a7c544 100644
--- a/Makefile
+++ b/Makefile
@@ -68,7 +68,7 @@ manifests: controller-gen
# Generate API reference documentation
api-docs: gen-crd-api-reference-docs
- $(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v1beta2 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/v1beta2/image-reflector.md
+ $(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v1beta3 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/v1beta3/image-reflector.md
# Run go mod tidy
tidy:
diff --git a/PROJECT b/PROJECT
index 87f3dcb3..22ff89dc 100644
--- a/PROJECT
+++ b/PROJECT
@@ -13,4 +13,10 @@ resources:
- group: image
kind: ImagePolicy
version: v1beta2
+- group: image
+ kind: ImageRepository
+ version: v1beta3
+- group: image
+ kind: ImagePolicy
+ version: v1beta3
version: "2"
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index e41e3304..47566a84 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
-Copyright 2022 The Flux authors
+Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/api/v1beta2/imagepolicy_types.go b/api/v1beta2/imagepolicy_types.go
index 4205664c..c4b9d6dd 100644
--- a/api/v1beta2/imagepolicy_types.go
+++ b/api/v1beta2/imagepolicy_types.go
@@ -42,24 +42,8 @@ type ImagePolicySpec struct {
// ordered and compared.
// +optional
FilterTags *TagFilter `json:"filterTags,omitempty"`
- // ReflectDigest governs the setting of the `.status.latestDigest` field.
- // +optional
- DigestReflectionPolicy *ReflectionPolicy `json:"digestReflectionPolicy,omitempty"`
}
-// ReflectionPolicy describes a policy for if/when to reflect a value from the registry in a certain resource field.
-// +kubebuilder:validation:Enum=Always;IfNotPresent
-type ReflectionPolicy string
-
-const (
- // ReflectAlways means that a value is always reflected with the latest value from the registry even if this would
- // overwrite an existing value in the object.
- ReflectAlways ReflectionPolicy = "Always"
- // ReflectIfNotPresent means that the target value is only reflected from the registry if it is empty. It will
- // never be overwritten afterwards, even if it changes in the registry.
- ReflectIfNotPresent ReflectionPolicy = "IfNotPresent"
-)
-
// ImagePolicyChoice is a union of all the types of policy that can be
// supplied.
type ImagePolicyChoice struct {
@@ -123,10 +107,6 @@ type ImagePolicyStatus struct {
// the image repository, when filtered and ordered according to
// the policy.
LatestImage string `json:"latestImage,omitempty"`
- // LatestDigest is the digest of the latest image stored in the
- // accompanying LatestImage field.
- // +optional
- LatestDigest string `json:"latestDigest,omitempty"`
// ObservedPreviousImage is the observed previous LatestImage. It is used
// to keep track of the previous and current images.
// +optional
@@ -147,7 +127,6 @@ func (p *ImagePolicy) SetConditions(conditions []metav1.Condition) {
p.Status.Conditions = conditions
}
-// +kubebuilder:storageversion
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="LatestImage",type=string,JSONPath=`.status.latestImage`
diff --git a/api/v1beta2/imagerepository_types.go b/api/v1beta2/imagerepository_types.go
index eaee2c14..0766509e 100644
--- a/api/v1beta2/imagerepository_types.go
+++ b/api/v1beta2/imagerepository_types.go
@@ -185,7 +185,6 @@ func (in ImageRepository) GetRequeueAfter() time.Duration {
return in.Spec.Interval.Duration
}
-// +kubebuilder:storageversion
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Last scan",type=string,JSONPath=`.status.lastScanResult.scanTime`
diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go
index 14399b9d..ba5d2dd6 100644
--- a/api/v1beta2/zz_generated.deepcopy.go
+++ b/api/v1beta2/zz_generated.deepcopy.go
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
-Copyright 2022 The Flux authors
+Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -142,11 +142,6 @@ func (in *ImagePolicySpec) DeepCopyInto(out *ImagePolicySpec) {
*out = new(TagFilter)
**out = **in
}
- if in.DigestReflectionPolicy != nil {
- in, out := &in.DigestReflectionPolicy, &out.DigestReflectionPolicy
- *out = new(ReflectionPolicy)
- **out = **in
- }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePolicySpec.
diff --git a/api/v1beta3/condition_types.go b/api/v1beta3/condition_types.go
new file mode 100644
index 00000000..373abb85
--- /dev/null
+++ b/api/v1beta3/condition_types.go
@@ -0,0 +1,35 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta3
+
+const ImageFinalizer = "finalizers.fluxcd.io"
+
+const (
+ // ImageURLInvalidReason represents the fact that a given repository has an invalid image URL.
+ ImageURLInvalidReason string = "ImageURLInvalid"
+
+ // DependencyNotReadyReason represents the fact that
+ // one of the dependencies is not ready.
+ DependencyNotReadyReason string = "DependencyNotReady"
+
+ // AuthenticationFailedReason signals that a Secret does not have the
+ // required fields, or the provided credentials do not match.
+ AuthenticationFailedReason string = "AuthenticationFailed"
+
+ // ReadOperationFailedReason signals a failure caused by a read operation.
+ ReadOperationFailedReason string = "ReadOperationFailed"
+)
diff --git a/api/v1beta3/doc.go b/api/v1beta3/doc.go
new file mode 100644
index 00000000..9329202e
--- /dev/null
+++ b/api/v1beta3/doc.go
@@ -0,0 +1,24 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package v1beta3 contains API types for the image API group, version
+// v1beta3. These types are concerned with reflecting metadata from
+// OCI image repositories into a cluster, so they can be consulted for
+// e.g., automation.
+//
+// +kubebuilder:object:generate=true
+// +groupName=image.toolkit.fluxcd.io
+package v1beta3
diff --git a/api/v1beta3/groupversion_info.go b/api/v1beta3/groupversion_info.go
new file mode 100644
index 00000000..cad8be83
--- /dev/null
+++ b/api/v1beta3/groupversion_info.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package v1beta3 contains API Schema definitions for the image v1beta2 API group
+// +kubebuilder:object:generate=true
+// +groupName=image.toolkit.fluxcd.io
+package v1beta3
+
+import (
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "sigs.k8s.io/controller-runtime/pkg/scheme"
+)
+
+var (
+ // GroupVersion is group version used to register these objects
+ GroupVersion = schema.GroupVersion{Group: "image.toolkit.fluxcd.io", Version: "v1beta3"}
+
+ // SchemeBuilder is used to add go types to the GroupVersionKind scheme
+ SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
+
+ // AddToScheme adds the types in this group-version to the given scheme.
+ AddToScheme = SchemeBuilder.AddToScheme
+)
diff --git a/api/v1beta3/imagepolicy_types.go b/api/v1beta3/imagepolicy_types.go
new file mode 100644
index 00000000..4385dd2e
--- /dev/null
+++ b/api/v1beta3/imagepolicy_types.go
@@ -0,0 +1,189 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta3
+
+import (
+ "github.com/fluxcd/pkg/apis/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const ImagePolicyKind = "ImagePolicy"
+const ImagePolicyFinalizer = "finalizers.fluxcd.io"
+
+// ImagePolicySpec defines the parameters for calculating the
+// ImagePolicy.
+type ImagePolicySpec struct {
+ // ImageRepositoryRef points at the object specifying the image
+ // being scanned
+ // +required
+ ImageRepositoryRef meta.NamespacedObjectReference `json:"imageRepositoryRef"`
+ // Policy gives the particulars of the policy to be followed in
+ // selecting the most recent image
+ // +required
+ Policy ImagePolicyChoice `json:"policy"`
+ // FilterTags enables filtering for only a subset of tags based on a set of
+ // rules. If no rules are provided, all the tags from the repository will be
+ // ordered and compared.
+ // +optional
+ FilterTags *TagFilter `json:"filterTags,omitempty"`
+ // ReflectDigest governs the setting of the `.status.latestDigest` field.
+ // +optional
+ DigestReflectionPolicy *ReflectionPolicy `json:"digestReflectionPolicy,omitempty"`
+}
+
+// ReflectionPolicy describes a policy for if/when to reflect a value from the registry in a certain resource field.
+// +kubebuilder:validation:Enum=Always;IfNotPresent
+type ReflectionPolicy string
+
+const (
+ // ReflectAlways means that a value is always reflected with the latest value from the registry even if this would
+ // overwrite an existing value in the object.
+ ReflectAlways ReflectionPolicy = "Always"
+ // ReflectIfNotPresent means that the target value is only reflected from the registry if it is empty. It will
+ // never be overwritten afterwards, even if it changes in the registry.
+ ReflectIfNotPresent ReflectionPolicy = "IfNotPresent"
+)
+
+// ImagePolicyChoice is a union of all the types of policy that can be
+// supplied.
+type ImagePolicyChoice struct {
+ // SemVer gives a semantic version range to check against the tags
+ // available.
+ // +optional
+ SemVer *SemVerPolicy `json:"semver,omitempty"`
+ // Alphabetical set of rules to use for alphabetical ordering of the tags.
+ // +optional
+ Alphabetical *AlphabeticalPolicy `json:"alphabetical,omitempty"`
+ // Numerical set of rules to use for numerical ordering of the tags.
+ // +optional
+ Numerical *NumericalPolicy `json:"numerical,omitempty"`
+}
+
+// SemVerPolicy specifies a semantic version policy.
+type SemVerPolicy struct {
+ // Range gives a semver range for the image tag; the highest
+ // version within the range that's a tag yields the latest image.
+ // +required
+ Range string `json:"range"`
+}
+
+// AlphabeticalPolicy specifies a alphabetical ordering policy.
+type AlphabeticalPolicy struct {
+ // Order specifies the sorting order of the tags. Given the letters of the
+ // alphabet as tags, ascending order would select Z, and descending order
+ // would select A.
+ // +kubebuilder:default:="asc"
+ // +kubebuilder:validation:Enum=asc;desc
+ // +optional
+ Order string `json:"order,omitempty"`
+}
+
+// NumericalPolicy specifies a numerical ordering policy.
+type NumericalPolicy struct {
+ // Order specifies the sorting order of the tags. Given the integer values
+ // from 0 to 9 as tags, ascending order would select 9, and descending order
+ // would select 0.
+ // +kubebuilder:default:="asc"
+ // +kubebuilder:validation:Enum=asc;desc
+ // +optional
+ Order string `json:"order,omitempty"`
+}
+
+// TagFilter enables filtering tags based on a set of defined rules
+type TagFilter struct {
+ // Pattern specifies a regular expression pattern used to filter for image
+ // tags.
+ // +optional
+ Pattern string `json:"pattern"`
+ // Extract allows a capture group to be extracted from the specified regular
+ // expression pattern, useful before tag evaluation.
+ // +optional
+ Extract string `json:"extract"`
+}
+
+// ImageRef represents an image reference.
+type ImageRef struct {
+ // Name is the bare image's name.
+ Name string `json:"image,omitempty"`
+ // Tag is the image's tag.
+ Tag string `json:"tag,omitempty"`
+ // Digest is the image's digest.
+ // +optional
+ Digest string `json:"digest,omitempty"`
+}
+
+func (r ImageRef) String() string {
+ res := r.Name + ":" + r.Tag
+ if r.Digest != "" {
+ res += "@" + r.Digest
+ }
+ return res
+}
+
+// ImagePolicyStatus defines the observed state of ImagePolicy
+type ImagePolicyStatus struct {
+ // LatestRef gives the first in the list of images scanned by
+ // the image repository, when filtered and ordered according
+ // to the policy.
+ LatestRef ImageRef `json:"latestRef,omitempty"`
+ // ObservedPreviousRef is the observed previous LatestRef. It is used
+ // to keep track of the previous and current images.
+ // +optional
+ ObservedPreviousRef *ImageRef `json:"observedPreviousRef,omitempty"`
+ // +optional
+ ObservedGeneration int64 `json:"observedGeneration,omitempty"`
+ // +optional
+ Conditions []metav1.Condition `json:"conditions,omitempty"`
+}
+
+// GetConditions returns the status conditions of the object.
+func (p ImagePolicy) GetConditions() []metav1.Condition {
+ return p.Status.Conditions
+}
+
+// SetConditions sets the status conditions on the object.
+func (p *ImagePolicy) SetConditions(conditions []metav1.Condition) {
+ p.Status.Conditions = conditions
+}
+
+// +kubebuilder:storageversion
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +kubebuilder:printcolumn:name="LatestImage",type=string,JSONPath=`.status.latestImage`
+
+// ImagePolicy is the Schema for the imagepolicies API
+type ImagePolicy struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ Spec ImagePolicySpec `json:"spec,omitempty"`
+ // +kubebuilder:default={"observedGeneration":-1}
+ Status ImagePolicyStatus `json:"status,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+
+// ImagePolicyList contains a list of ImagePolicy
+type ImagePolicyList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+ Items []ImagePolicy `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&ImagePolicy{}, &ImagePolicyList{})
+}
diff --git a/api/v1beta3/imagerepository_types.go b/api/v1beta3/imagerepository_types.go
new file mode 100644
index 00000000..1566dc65
--- /dev/null
+++ b/api/v1beta3/imagerepository_types.go
@@ -0,0 +1,209 @@
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta3
+
+import (
+ "time"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ "github.com/fluxcd/pkg/apis/acl"
+ "github.com/fluxcd/pkg/apis/meta"
+)
+
+const ImageRepositoryKind = "ImageRepository"
+const ImageRepositoryFinalizer = "finalizers.fluxcd.io"
+
+// ImageRepositorySpec defines the parameters for scanning an image
+// repository, e.g., `fluxcd/flux`.
+type ImageRepositorySpec struct {
+ // Image is the name of the image repository
+ // +required
+ Image string `json:"image,omitempty"`
+ // Interval is the length of time to wait between
+ // scans of the image repository.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
+ // +required
+ Interval metav1.Duration `json:"interval,omitempty"`
+
+ // Timeout for image scanning.
+ // Defaults to 'Interval' duration.
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m))+$"
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+ // SecretRef can be given the name of a secret containing
+ // credentials to use for the image registry. The secret should be
+ // created with `kubectl create secret docker-registry`, or the
+ // equivalent.
+ // +optional
+ SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
+
+ // ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
+ // the image pull if the service account has attached pull secrets.
+ // +kubebuilder:validation:MaxLength=253
+ // +optional
+ ServiceAccountName string `json:"serviceAccountName,omitempty"`
+
+ // CertSecretRef can be given the name of a secret containing
+ // either or both of
+ //
+ // - a PEM-encoded client certificate (`certFile`) and private
+ // key (`keyFile`);
+ // - a PEM-encoded CA certificate (`caFile`)
+ //
+ // and whichever are supplied, will be used for connecting to the
+ // registry. The client cert and key are useful if you are
+ // authenticating with a certificate; the CA cert is useful if
+ // you are using a self-signed server certificate.
+ // +optional
+ CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"`
+
+ // This flag tells the controller to suspend subsequent image scans.
+ // It does not apply to already started scans. Defaults to false.
+ // +optional
+ Suspend bool `json:"suspend,omitempty"`
+
+ // AccessFrom defines an ACL for allowing cross-namespace references
+ // to the ImageRepository object based on the caller's namespace labels.
+ // +optional
+ AccessFrom *acl.AccessFrom `json:"accessFrom,omitempty"`
+
+ // ExclusionList is a list of regex strings used to exclude certain tags
+ // from being stored in the database.
+ // +kubebuilder:default:={"^.*\\.sig$"}
+ // +kubebuilder:validation:MaxItems:=25
+ // +optional
+ ExclusionList []string `json:"exclusionList,omitempty"`
+
+ // The provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'.
+ // When not specified, defaults to 'generic'.
+ // +kubebuilder:validation:Enum=generic;aws;azure;gcp
+ // +kubebuilder:default:=generic
+ // +optional
+ Provider string `json:"provider,omitempty"`
+}
+
+type ScanResult struct {
+ TagCount int `json:"tagCount"`
+ ScanTime metav1.Time `json:"scanTime,omitempty"`
+ LatestTags []string `json:"latestTags,omitempty"`
+}
+
+// ImageRepositoryStatus defines the observed state of ImageRepository
+type ImageRepositoryStatus struct {
+ // +optional
+ Conditions []metav1.Condition `json:"conditions,omitempty"`
+
+ // ObservedGeneration is the last reconciled generation.
+ // +optional
+ ObservedGeneration int64 `json:"observedGeneration,omitempty"`
+
+ // CanonicalName is the name of the image repository with all the
+ // implied bits made explicit; e.g., `docker.io/library/alpine`
+ // rather than `alpine`.
+ // +optional
+ CanonicalImageName string `json:"canonicalImageName,omitempty"`
+
+ // LastScanResult contains the number of fetched tags.
+ // +optional
+ LastScanResult *ScanResult `json:"lastScanResult,omitempty"`
+
+ // ObservedExclusionList is a list of observed exclusion list. It reflects
+ // the exclusion rules used for the observed scan result in
+ // spec.lastScanResult.
+ ObservedExclusionList []string `json:"observedExclusionList,omitempty"`
+
+ meta.ReconcileRequestStatus `json:",inline"`
+}
+
+// GetTimeout returns the timeout with default.
+func (in ImageRepository) GetTimeout() time.Duration {
+ duration := in.Spec.Interval.Duration
+ if in.Spec.Timeout != nil {
+ duration = in.Spec.Timeout.Duration
+ }
+ if duration < time.Second {
+ return time.Second
+ }
+ return duration
+}
+
+// GetExclusionList returns the exclusion list with default.
+func (in ImageRepository) GetExclusionList() []string {
+ el := []string{"^.*\\.sig$"}
+ if len(in.Spec.ExclusionList) > 0 {
+ el = in.Spec.ExclusionList
+ }
+ return el
+}
+
+// GetProvider returns the provider with default.
+func (in ImageRepository) GetProvider() string {
+ p := "generic"
+ if in.Spec.Provider != "" {
+ p = in.Spec.Provider
+ }
+ return p
+}
+
+// GetConditions returns the status conditions of the object.
+func (in ImageRepository) GetConditions() []metav1.Condition {
+ return in.Status.Conditions
+}
+
+// SetConditions sets the status conditions on the object.
+func (in *ImageRepository) SetConditions(conditions []metav1.Condition) {
+ in.Status.Conditions = conditions
+}
+
+// GetRequeueAfter returns the duration after which the ImageRepository must be
+// reconciled again.
+func (in ImageRepository) GetRequeueAfter() time.Duration {
+ return in.Spec.Interval.Duration
+}
+
+// +kubebuilder:storageversion
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +kubebuilder:printcolumn:name="Last scan",type=string,JSONPath=`.status.lastScanResult.scanTime`
+// +kubebuilder:printcolumn:name="Tags",type=string,JSONPath=`.status.lastScanResult.tagCount`
+
+// ImageRepository is the Schema for the imagerepositories API
+type ImageRepository struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ Spec ImageRepositorySpec `json:"spec,omitempty"`
+ // +kubebuilder:default={"observedGeneration":-1}
+ Status ImageRepositoryStatus `json:"status,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+
+// ImageRepositoryList contains a list of ImageRepository
+type ImageRepositoryList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+ Items []ImageRepository `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&ImageRepository{}, &ImageRepositoryList{})
+}
diff --git a/api/v1beta3/zz_generated.deepcopy.go b/api/v1beta3/zz_generated.deepcopy.go
new file mode 100644
index 00000000..f6acc381
--- /dev/null
+++ b/api/v1beta3/zz_generated.deepcopy.go
@@ -0,0 +1,402 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright 2023 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1beta3
+
+import (
+ "github.com/fluxcd/pkg/apis/acl"
+ "github.com/fluxcd/pkg/apis/meta"
+ "k8s.io/apimachinery/pkg/apis/meta/v1"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AlphabeticalPolicy) DeepCopyInto(out *AlphabeticalPolicy) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlphabeticalPolicy.
+func (in *AlphabeticalPolicy) DeepCopy() *AlphabeticalPolicy {
+ if in == nil {
+ return nil
+ }
+ out := new(AlphabeticalPolicy)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImagePolicy) DeepCopyInto(out *ImagePolicy) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePolicy.
+func (in *ImagePolicy) DeepCopy() *ImagePolicy {
+ if in == nil {
+ return nil
+ }
+ out := new(ImagePolicy)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ImagePolicy) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImagePolicyChoice) DeepCopyInto(out *ImagePolicyChoice) {
+ *out = *in
+ if in.SemVer != nil {
+ in, out := &in.SemVer, &out.SemVer
+ *out = new(SemVerPolicy)
+ **out = **in
+ }
+ if in.Alphabetical != nil {
+ in, out := &in.Alphabetical, &out.Alphabetical
+ *out = new(AlphabeticalPolicy)
+ **out = **in
+ }
+ if in.Numerical != nil {
+ in, out := &in.Numerical, &out.Numerical
+ *out = new(NumericalPolicy)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePolicyChoice.
+func (in *ImagePolicyChoice) DeepCopy() *ImagePolicyChoice {
+ if in == nil {
+ return nil
+ }
+ out := new(ImagePolicyChoice)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImagePolicyList) DeepCopyInto(out *ImagePolicyList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]ImagePolicy, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePolicyList.
+func (in *ImagePolicyList) DeepCopy() *ImagePolicyList {
+ if in == nil {
+ return nil
+ }
+ out := new(ImagePolicyList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ImagePolicyList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImagePolicySpec) DeepCopyInto(out *ImagePolicySpec) {
+ *out = *in
+ out.ImageRepositoryRef = in.ImageRepositoryRef
+ in.Policy.DeepCopyInto(&out.Policy)
+ if in.FilterTags != nil {
+ in, out := &in.FilterTags, &out.FilterTags
+ *out = new(TagFilter)
+ **out = **in
+ }
+ if in.DigestReflectionPolicy != nil {
+ in, out := &in.DigestReflectionPolicy, &out.DigestReflectionPolicy
+ *out = new(ReflectionPolicy)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePolicySpec.
+func (in *ImagePolicySpec) DeepCopy() *ImagePolicySpec {
+ if in == nil {
+ return nil
+ }
+ out := new(ImagePolicySpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImagePolicyStatus) DeepCopyInto(out *ImagePolicyStatus) {
+ *out = *in
+ out.LatestRef = in.LatestRef
+ if in.ObservedPreviousRef != nil {
+ in, out := &in.ObservedPreviousRef, &out.ObservedPreviousRef
+ *out = new(ImageRef)
+ **out = **in
+ }
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]v1.Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePolicyStatus.
+func (in *ImagePolicyStatus) DeepCopy() *ImagePolicyStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(ImagePolicyStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImageRef) DeepCopyInto(out *ImageRef) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageRef.
+func (in *ImageRef) DeepCopy() *ImageRef {
+ if in == nil {
+ return nil
+ }
+ out := new(ImageRef)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImageRepository) DeepCopyInto(out *ImageRepository) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageRepository.
+func (in *ImageRepository) DeepCopy() *ImageRepository {
+ if in == nil {
+ return nil
+ }
+ out := new(ImageRepository)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ImageRepository) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImageRepositoryList) DeepCopyInto(out *ImageRepositoryList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]ImageRepository, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageRepositoryList.
+func (in *ImageRepositoryList) DeepCopy() *ImageRepositoryList {
+ if in == nil {
+ return nil
+ }
+ out := new(ImageRepositoryList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ImageRepositoryList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImageRepositorySpec) DeepCopyInto(out *ImageRepositorySpec) {
+ *out = *in
+ out.Interval = in.Interval
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(v1.Duration)
+ **out = **in
+ }
+ if in.SecretRef != nil {
+ in, out := &in.SecretRef, &out.SecretRef
+ *out = new(meta.LocalObjectReference)
+ **out = **in
+ }
+ if in.CertSecretRef != nil {
+ in, out := &in.CertSecretRef, &out.CertSecretRef
+ *out = new(meta.LocalObjectReference)
+ **out = **in
+ }
+ if in.AccessFrom != nil {
+ in, out := &in.AccessFrom, &out.AccessFrom
+ *out = new(acl.AccessFrom)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.ExclusionList != nil {
+ in, out := &in.ExclusionList, &out.ExclusionList
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageRepositorySpec.
+func (in *ImageRepositorySpec) DeepCopy() *ImageRepositorySpec {
+ if in == nil {
+ return nil
+ }
+ out := new(ImageRepositorySpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImageRepositoryStatus) DeepCopyInto(out *ImageRepositoryStatus) {
+ *out = *in
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]v1.Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.LastScanResult != nil {
+ in, out := &in.LastScanResult, &out.LastScanResult
+ *out = new(ScanResult)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.ObservedExclusionList != nil {
+ in, out := &in.ObservedExclusionList, &out.ObservedExclusionList
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ out.ReconcileRequestStatus = in.ReconcileRequestStatus
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageRepositoryStatus.
+func (in *ImageRepositoryStatus) DeepCopy() *ImageRepositoryStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(ImageRepositoryStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NumericalPolicy) DeepCopyInto(out *NumericalPolicy) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NumericalPolicy.
+func (in *NumericalPolicy) DeepCopy() *NumericalPolicy {
+ if in == nil {
+ return nil
+ }
+ out := new(NumericalPolicy)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ScanResult) DeepCopyInto(out *ScanResult) {
+ *out = *in
+ in.ScanTime.DeepCopyInto(&out.ScanTime)
+ if in.LatestTags != nil {
+ in, out := &in.LatestTags, &out.LatestTags
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScanResult.
+func (in *ScanResult) DeepCopy() *ScanResult {
+ if in == nil {
+ return nil
+ }
+ out := new(ScanResult)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SemVerPolicy) DeepCopyInto(out *SemVerPolicy) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SemVerPolicy.
+func (in *SemVerPolicy) DeepCopy() *SemVerPolicy {
+ if in == nil {
+ return nil
+ }
+ out := new(SemVerPolicy)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TagFilter) DeepCopyInto(out *TagFilter) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TagFilter.
+func (in *TagFilter) DeepCopy() *TagFilter {
+ if in == nil {
+ return nil
+ }
+ out := new(TagFilter)
+ in.DeepCopyInto(out)
+ return out
+}
diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml
index 52d54a8c..110416dc 100644
--- a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml
+++ b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml
@@ -210,6 +210,201 @@ spec:
name: LatestImage
type: string
name: v1beta2
+ schema:
+ openAPIV3Schema:
+ description: ImagePolicy is the Schema for the imagepolicies API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: ImagePolicySpec defines the parameters for calculating the
+ ImagePolicy.
+ properties:
+ filterTags:
+ description: FilterTags enables filtering for only a subset of tags
+ based on a set of rules. If no rules are provided, all the tags
+ from the repository will be ordered and compared.
+ properties:
+ extract:
+ description: Extract allows a capture group to be extracted from
+ the specified regular expression pattern, useful before tag
+ evaluation.
+ type: string
+ pattern:
+ description: Pattern specifies a regular expression pattern used
+ to filter for image tags.
+ type: string
+ type: object
+ imageRepositoryRef:
+ description: ImageRepositoryRef points at the object specifying the
+ image being scanned
+ properties:
+ name:
+ description: Name of the referent.
+ type: string
+ namespace:
+ description: Namespace of the referent, when not specified it
+ acts as LocalObjectReference.
+ type: string
+ required:
+ - name
+ type: object
+ policy:
+ description: Policy gives the particulars of the policy to be followed
+ in selecting the most recent image
+ properties:
+ alphabetical:
+ description: Alphabetical set of rules to use for alphabetical
+ ordering of the tags.
+ properties:
+ order:
+ default: asc
+ description: Order specifies the sorting order of the tags.
+ Given the letters of the alphabet as tags, ascending order
+ would select Z, and descending order would select A.
+ enum:
+ - asc
+ - desc
+ type: string
+ type: object
+ numerical:
+ description: Numerical set of rules to use for numerical ordering
+ of the tags.
+ properties:
+ order:
+ default: asc
+ description: Order specifies the sorting order of the tags.
+ Given the integer values from 0 to 9 as tags, ascending
+ order would select 9, and descending order would select
+ 0.
+ enum:
+ - asc
+ - desc
+ type: string
+ type: object
+ semver:
+ description: SemVer gives a semantic version range to check against
+ the tags available.
+ properties:
+ range:
+ description: Range gives a semver range for the image tag;
+ the highest version within the range that's a tag yields
+ the latest image.
+ type: string
+ required:
+ - range
+ type: object
+ type: object
+ required:
+ - imageRepositoryRef
+ - policy
+ type: object
+ status:
+ default:
+ observedGeneration: -1
+ description: ImagePolicyStatus defines the observed state of ImagePolicy
+ properties:
+ conditions:
+ items:
+ description: "Condition contains details for one aspect of the current
+ state of this API Resource. --- This struct is intended for direct
+ use as an array at the field path .status.conditions. For example,
+ \n type FooStatus struct{ // Represents the observations of a
+ foo's current state. // Known .status.conditions.type are: \"Available\",
+ \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
+ // +listType=map // +listMapKey=type Conditions []metav1.Condition
+ `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+ protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+ properties:
+ lastTransitionTime:
+ description: lastTransitionTime is the last time the condition
+ transitioned from one status to another. This should be when
+ the underlying condition changed. If that is not known, then
+ using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: message is a human readable message indicating
+ details about the transition. This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: observedGeneration represents the .metadata.generation
+ that the condition was set based upon. For instance, if .metadata.generation
+ is currently 12, but the .status.conditions[x].observedGeneration
+ is 9, the condition is out of date with respect to the current
+ state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: reason contains a programmatic identifier indicating
+ the reason for the condition's last transition. Producers
+ of specific condition types may define expected values and
+ meanings for this field, and whether the values are considered
+ a guaranteed API. The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ --- Many .condition.type values are consistent across resources
+ like Available, but because arbitrary conditions can be useful
+ (see .node.status.conditions), the ability to deconflict is
+ important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ latestImage:
+ description: LatestImage gives the first in the list of images scanned
+ by the image repository, when filtered and ordered according to
+ the policy.
+ type: string
+ observedGeneration:
+ format: int64
+ type: integer
+ observedPreviousImage:
+ description: ObservedPreviousImage is the observed previous LatestImage.
+ It is used to keep track of the previous and current images.
+ type: string
+ type: object
+ type: object
+ served: true
+ storage: false
+ subresources:
+ status: {}
+ - additionalPrinterColumns:
+ - jsonPath: .status.latestImage
+ name: LatestImage
+ type: string
+ name: v1beta3
schema:
openAPIV3Schema:
description: ImagePolicy is the Schema for the imagepolicies API
@@ -389,22 +584,38 @@ spec:
- type
type: object
type: array
- latestDigest:
- description: LatestDigest is the digest of the latest image stored
- in the accompanying LatestImage field.
- type: string
- latestImage:
- description: LatestImage gives the first in the list of images scanned
+ latestRef:
+ description: LatestRef gives the first in the list of images scanned
by the image repository, when filtered and ordered according to
the policy.
- type: string
+ properties:
+ digest:
+ description: Digest is the image's digest.
+ type: string
+ image:
+ description: Name is the bare image's name.
+ type: string
+ tag:
+ description: Tag is the image's tag.
+ type: string
+ type: object
observedGeneration:
format: int64
type: integer
- observedPreviousImage:
- description: ObservedPreviousImage is the observed previous LatestImage.
+ observedPreviousRef:
+ description: ObservedPreviousRef is the observed previous LatestRef.
It is used to keep track of the previous and current images.
- type: string
+ properties:
+ digest:
+ description: Digest is the image's digest.
+ type: string
+ image:
+ description: Name is the bare image's name.
+ type: string
+ tag:
+ description: Tag is the image's tag.
+ type: string
+ type: object
type: object
type: object
served: true
diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml
index 40075d79..82528b04 100644
--- a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml
+++ b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml
@@ -469,6 +469,246 @@ spec:
type: object
type: object
served: true
+ storage: false
+ subresources:
+ status: {}
+ - additionalPrinterColumns:
+ - jsonPath: .status.lastScanResult.scanTime
+ name: Last scan
+ type: string
+ - jsonPath: .status.lastScanResult.tagCount
+ name: Tags
+ type: string
+ name: v1beta3
+ schema:
+ openAPIV3Schema:
+ description: ImageRepository is the Schema for the imagerepositories API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: ImageRepositorySpec defines the parameters for scanning an
+ image repository, e.g., `fluxcd/flux`.
+ properties:
+ accessFrom:
+ description: AccessFrom defines an ACL for allowing cross-namespace
+ references to the ImageRepository object based on the caller's namespace
+ labels.
+ properties:
+ namespaceSelectors:
+ description: NamespaceSelectors is the list of namespace selectors
+ to which this ACL applies. Items in this list are evaluated
+ using a logical OR operation.
+ items:
+ description: NamespaceSelector selects the namespaces to which
+ this ACL applies. An empty map of MatchLabels matches all
+ namespaces in a cluster.
+ properties:
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: MatchLabels is a map of {key,value} pairs.
+ A single {key,value} in the matchLabels map is equivalent
+ to an element of matchExpressions, whose key field is
+ "key", the operator is "In", and the values array contains
+ only "value". The requirements are ANDed.
+ type: object
+ type: object
+ type: array
+ required:
+ - namespaceSelectors
+ type: object
+ certSecretRef:
+ description: "CertSecretRef can be given the name of a secret containing
+ either or both of \n - a PEM-encoded client certificate (`certFile`)
+ and private key (`keyFile`); - a PEM-encoded CA certificate (`caFile`)
+ \n and whichever are supplied, will be used for connecting to the
+ registry. The client cert and key are useful if you are authenticating
+ with a certificate; the CA cert is useful if you are using a self-signed
+ server certificate."
+ properties:
+ name:
+ description: Name of the referent.
+ type: string
+ required:
+ - name
+ type: object
+ exclusionList:
+ default:
+ - ^.*\.sig$
+ description: ExclusionList is a list of regex strings used to exclude
+ certain tags from being stored in the database.
+ items:
+ type: string
+ maxItems: 25
+ type: array
+ image:
+ description: Image is the name of the image repository
+ type: string
+ interval:
+ description: Interval is the length of time to wait between scans
+ of the image repository.
+ pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
+ type: string
+ provider:
+ default: generic
+ description: The provider used for authentication, can be 'aws', 'azure',
+ 'gcp' or 'generic'. When not specified, defaults to 'generic'.
+ enum:
+ - generic
+ - aws
+ - azure
+ - gcp
+ type: string
+ secretRef:
+ description: SecretRef can be given the name of a secret containing
+ credentials to use for the image registry. The secret should be
+ created with `kubectl create secret docker-registry`, or the equivalent.
+ properties:
+ name:
+ description: Name of the referent.
+ type: string
+ required:
+ - name
+ type: object
+ serviceAccountName:
+ description: ServiceAccountName is the name of the Kubernetes ServiceAccount
+ used to authenticate the image pull if the service account has attached
+ pull secrets.
+ maxLength: 253
+ type: string
+ suspend:
+ description: This flag tells the controller to suspend subsequent
+ image scans. It does not apply to already started scans. Defaults
+ to false.
+ type: boolean
+ timeout:
+ description: Timeout for image scanning. Defaults to 'Interval' duration.
+ pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$
+ type: string
+ type: object
+ status:
+ default:
+ observedGeneration: -1
+ description: ImageRepositoryStatus defines the observed state of ImageRepository
+ properties:
+ canonicalImageName:
+ description: CanonicalName is the name of the image repository with
+ all the implied bits made explicit; e.g., `docker.io/library/alpine`
+ rather than `alpine`.
+ type: string
+ conditions:
+ items:
+ description: "Condition contains details for one aspect of the current
+ state of this API Resource. --- This struct is intended for direct
+ use as an array at the field path .status.conditions. For example,
+ \n type FooStatus struct{ // Represents the observations of a
+ foo's current state. // Known .status.conditions.type are: \"Available\",
+ \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
+ // +listType=map // +listMapKey=type Conditions []metav1.Condition
+ `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+ protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+ properties:
+ lastTransitionTime:
+ description: lastTransitionTime is the last time the condition
+ transitioned from one status to another. This should be when
+ the underlying condition changed. If that is not known, then
+ using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: message is a human readable message indicating
+ details about the transition. This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: observedGeneration represents the .metadata.generation
+ that the condition was set based upon. For instance, if .metadata.generation
+ is currently 12, but the .status.conditions[x].observedGeneration
+ is 9, the condition is out of date with respect to the current
+ state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: reason contains a programmatic identifier indicating
+ the reason for the condition's last transition. Producers
+ of specific condition types may define expected values and
+ meanings for this field, and whether the values are considered
+ a guaranteed API. The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ --- Many .condition.type values are consistent across resources
+ like Available, but because arbitrary conditions can be useful
+ (see .node.status.conditions), the ability to deconflict is
+ important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ lastHandledReconcileAt:
+ description: LastHandledReconcileAt holds the value of the most recent
+ reconcile request value, so a change of the annotation value can
+ be detected.
+ type: string
+ lastScanResult:
+ description: LastScanResult contains the number of fetched tags.
+ properties:
+ latestTags:
+ items:
+ type: string
+ type: array
+ scanTime:
+ format: date-time
+ type: string
+ tagCount:
+ type: integer
+ required:
+ - tagCount
+ type: object
+ observedExclusionList:
+ description: ObservedExclusionList is a list of observed exclusion
+ list. It reflects the exclusion rules used for the observed scan
+ result in spec.lastScanResult.
+ items:
+ type: string
+ type: array
+ observedGeneration:
+ description: ObservedGeneration is the last reconciled generation.
+ format: int64
+ type: integer
+ type: object
+ type: object
+ served: true
storage: true
subresources:
status: {}
diff --git a/config/samples/image_v1beta3_imagepolicy.yaml b/config/samples/image_v1beta3_imagepolicy.yaml
new file mode 100644
index 00000000..99d3e340
--- /dev/null
+++ b/config/samples/image_v1beta3_imagepolicy.yaml
@@ -0,0 +1,11 @@
+apiVersion: image.toolkit.fluxcd.io/v1beta3
+kind: ImagePolicy
+metadata:
+ name: imagepolicy-sample
+ namespace: flux-system
+spec:
+ imageRepositoryRef:
+ name: podinfo
+ policy:
+ semver:
+ range: 5.0.x
diff --git a/config/samples/image_v1beta3_imagerepository.yaml b/config/samples/image_v1beta3_imagerepository.yaml
new file mode 100644
index 00000000..1a0c4ade
--- /dev/null
+++ b/config/samples/image_v1beta3_imagerepository.yaml
@@ -0,0 +1,12 @@
+apiVersion: image.toolkit.fluxcd.io/v1beta3
+kind: ImageRepository
+metadata:
+ name: imagerepository-sample
+ namespace: flux-system
+spec:
+ image: ghcr.io/stefanprodan/podinfo
+ interval: 1m0s
+ accessFrom:
+ namespaceSelectors:
+ - matchLabels:
+ kubernetes.io/metadata.name: flux-system
diff --git a/docs/api/v1beta2/image-reflector.md b/docs/api/v1beta3/image-reflector.md
similarity index 82%
rename from docs/api/v1beta2/image-reflector.md
rename to docs/api/v1beta3/image-reflector.md
index 7dd8af52..89e246de 100644
--- a/docs/api/v1beta2/image-reflector.md
+++ b/docs/api/v1beta3/image-reflector.md
@@ -1,22 +1,22 @@
-Image reflector API reference v1beta2
+Image reflector API reference v1beta3
Packages:
-
-Package v1beta2 contains API types for the image API group, version
-v1beta2. These types are concerned with reflecting metadata from
+
+Package v1beta3 contains API types for the image API group, version
+v1beta3. These types are concerned with reflecting metadata from
OCI image repositories into a cluster, so they can be consulted for
e.g., automation.
Resource Types:
-
(Appears on:
-ImagePolicyChoice)
+ImagePolicyChoice)
AlphabeticalPolicy specifies a alphabetical ordering policy.
@@ -47,7 +47,7 @@ would select A.
-ImagePolicy
+ImagePolicy
ImagePolicy is the Schema for the imagepolicies API
@@ -78,7 +78,7 @@ Refer to the Kubernetes API documentation for the fields of the
spec
-
+
ImagePolicySpec
@@ -105,7 +105,7 @@ being scanned
|
policy
-
+
ImagePolicyChoice
@@ -119,7 +119,7 @@ selecting the most recent image
|
filterTags
-
+
TagFilter
@@ -135,7 +135,7 @@ ordered and compared.
|
digestReflectionPolicy
-
+
ReflectionPolicy
@@ -152,7 +152,7 @@ ReflectionPolicy
|
status
-
+
ImagePolicyStatus
@@ -164,11 +164,11 @@ ImagePolicyStatus
-ImagePolicyChoice
+ImagePolicyChoice
(Appears on:
-ImagePolicySpec)
+ImagePolicySpec)
ImagePolicyChoice is a union of all the types of policy that can be
supplied.
@@ -186,7 +186,7 @@ supplied.
|
semver
-
+
SemVerPolicy
@@ -201,7 +201,7 @@ available.
|
alphabetical
-
+
AlphabeticalPolicy
@@ -215,7 +215,7 @@ AlphabeticalPolicy
|
numerical
-
+
NumericalPolicy
@@ -229,11 +229,11 @@ NumericalPolicy
-ImagePolicySpec
+ImagePolicySpec
(Appears on:
-ImagePolicy)
+ImagePolicy)
ImagePolicySpec defines the parameters for calculating the
ImagePolicy.
@@ -265,7 +265,7 @@ being scanned
|
policy
-
+
ImagePolicyChoice
@@ -279,7 +279,7 @@ selecting the most recent image
|
filterTags
-
+
TagFilter
@@ -295,7 +295,7 @@ ordered and compared.
|
digestReflectionPolicy
-
+
ReflectionPolicy
@@ -309,11 +309,11 @@ ReflectionPolicy
-ImagePolicyStatus
+ImagePolicyStatus
(Appears on:
-ImagePolicy)
+ImagePolicy)
ImagePolicyStatus defines the observed state of ImagePolicy
+
+ImageRef
+
+
+(Appears on:
+ImagePolicyStatus)
+
+ImageRef represents an image reference.
+
-ImageRepository
+ImageRepository
ImageRepository is the Schema for the imagerepositories API
@@ -424,7 +470,7 @@ Refer to the Kubernetes API documentation for the fields of the
spec
-
+
ImageRepositorySpec
@@ -591,7 +637,7 @@ When not specified, defaults to ‘generic’.
|
status
-
+
ImageRepositoryStatus
@@ -603,11 +649,11 @@ ImageRepositoryStatus
-ImageRepositorySpec
+ImageRepositorySpec
(Appears on:
-ImageRepository)
+ImageRepository)
ImageRepositorySpec defines the parameters for scanning an image
repository, e.g., fluxcd/flux .
@@ -776,11 +822,11 @@ When not specified, defaults to ‘generic’.
-ImageRepositoryStatus
+ImageRepositoryStatus
(Appears on:
-ImageRepository)
+ImageRepository)
ImageRepositoryStatus defines the observed state of ImageRepository
@@ -836,7 +882,7 @@ rather than alpine .
lastScanResult
-
+
ScanResult
@@ -878,11 +924,11 @@ github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
-NumericalPolicy
+NumericalPolicy
(Appears on:
-ImagePolicyChoice)
+ImagePolicyChoice)
NumericalPolicy specifies a numerical ordering policy.
@@ -913,18 +959,18 @@ would select 0.
-ReflectionPolicy
+ReflectionPolicy
(string alias)
(Appears on:
-ImagePolicySpec)
+ImagePolicySpec)
ReflectionPolicy describes a policy for if/when to reflect a value from the registry in a certain resource field.
-ScanResult
+ScanResult
(Appears on:
-ImageRepositoryStatus)
+ImageRepositoryStatus)
-SemVerPolicy
+SemVerPolicy
(Appears on:
-ImagePolicyChoice)
+ImagePolicyChoice)
SemVerPolicy specifies a semantic version policy.
@@ -1005,11 +1051,11 @@ version within the range that’s a tag yields the latest image.
-TagFilter
+TagFilter
(Appears on:
-ImagePolicySpec)
+ImagePolicySpec)
TagFilter enables filtering tags based on a set of defined rules
-ImagePolicy
+ImagePolicy
ImagePolicy is the Schema for the imagepolicies API
@@ -78,7 +78,7 @@ Refer to the Kubernetes API documentation for the fields of the
spec
-
+
ImagePolicySpec
@@ -105,7 +105,7 @@ being scanned
|
policy
-
+
ImagePolicyChoice
@@ -119,7 +119,7 @@ selecting the most recent image
|
filterTags
-
+
TagFilter
@@ -135,7 +135,7 @@ ordered and compared.
|
digestReflectionPolicy
-
+
ReflectionPolicy
@@ -152,7 +152,7 @@ ReflectionPolicy
|
status
-
+
ImagePolicyStatus
@@ -164,11 +164,11 @@ ImagePolicyStatus
-ImagePolicyChoice
+ImagePolicyChoice
(Appears on:
-ImagePolicySpec)
+ImagePolicySpec)
ImagePolicyChoice is a union of all the types of policy that can be
supplied.
@@ -186,7 +186,7 @@ supplied.
|
semver
-
+
SemVerPolicy
@@ -201,7 +201,7 @@ available.
|
alphabetical
-
+
AlphabeticalPolicy
@@ -215,7 +215,7 @@ AlphabeticalPolicy
|
numerical
-
+
NumericalPolicy
@@ -229,11 +229,11 @@ NumericalPolicy
-ImagePolicySpec
+ImagePolicySpec
(Appears on:
-ImagePolicy)
+ImagePolicy)
ImagePolicySpec defines the parameters for calculating the
ImagePolicy.
@@ -265,7 +265,7 @@ being scanned
|
policy
-
+
ImagePolicyChoice
@@ -279,7 +279,7 @@ selecting the most recent image
|
filterTags
-
+
TagFilter
@@ -295,7 +295,7 @@ ordered and compared.
|
digestReflectionPolicy
-
+
ReflectionPolicy
@@ -309,11 +309,11 @@ ReflectionPolicy
-ImagePolicyStatus
+ImagePolicyStatus
(Appears on:
-ImagePolicy)
+ImagePolicy)
ImagePolicyStatus defines the observed state of ImagePolicy
@@ -328,9 +328,37 @@ ReflectionPolicy
+latestImage
+
+string
+
+ |
+
+ LatestImage gives the first in the list of images scanned by
+the image repository, when filtered and ordered according to
+the policy.
+Deprecated: Replaced by the composite “latestRef” field.
+ |
+
+
+
+observedPreviousImage
+
+string
+
+ |
+
+(Optional)
+ ObservedPreviousImage is the observed previous LatestImage. It is used
+to keep track of the previous and current images.
+Deprecated: Replaced by the composite “observedPreviousRef” field.
+ |
+
+
+
latestRef
-
+
ImageRef
@@ -345,7 +373,7 @@ to the policy.
|
observedPreviousRef
-
+
ImageRef
@@ -384,11 +412,11 @@ int64
-ImageRef
+ImageRef
(Appears on:
-ImagePolicyStatus)
+ImagePolicyStatus)
ImageRef represents an image reference.
@@ -439,7 +467,7 @@ string
-ImageRepository
+ImageRepository
ImageRepository is the Schema for the imagerepositories API
@@ -470,7 +498,7 @@ Refer to the Kubernetes API documentation for the fields of the
spec
-
+
ImageRepositorySpec
@@ -637,7 +665,7 @@ When not specified, defaults to ‘generic’.
|
status
-
+
ImageRepositoryStatus
@@ -649,11 +677,11 @@ ImageRepositoryStatus
-ImageRepositorySpec
+ImageRepositorySpec
(Appears on:
-ImageRepository)
+ImageRepository)
ImageRepositorySpec defines the parameters for scanning an image
repository, e.g., fluxcd/flux .
@@ -822,11 +850,11 @@ When not specified, defaults to ‘generic’.
-ImageRepositoryStatus
+ImageRepositoryStatus
(Appears on:
-ImageRepository)
+ImageRepository)
ImageRepositoryStatus defines the observed state of ImageRepository
@@ -882,7 +910,7 @@ rather than alpine .
lastScanResult
-
+
ScanResult
@@ -924,11 +952,11 @@ github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
-NumericalPolicy
+NumericalPolicy
(Appears on:
-ImagePolicyChoice)
+ImagePolicyChoice)
NumericalPolicy specifies a numerical ordering policy.
@@ -959,18 +987,18 @@ would select 0.
-ReflectionPolicy
+ReflectionPolicy
(string alias)
(Appears on:
-ImagePolicySpec)
+ImagePolicySpec)
ReflectionPolicy describes a policy for if/when to reflect a value from the registry in a certain resource field.
-ScanResult
+ScanResult
(Appears on:
-ImageRepositoryStatus)
+ImageRepositoryStatus)
-SemVerPolicy
+SemVerPolicy
(Appears on:
-ImagePolicyChoice)
+ImagePolicyChoice)
SemVerPolicy specifies a semantic version policy.
@@ -1051,11 +1079,11 @@ version within the range that’s a tag yields the latest image.
-TagFilter
+TagFilter
(Appears on:
-ImagePolicySpec)
+ImagePolicySpec)
TagFilter enables filtering tags based on a set of defined rules
|
(Optional)
- ReflectDigest governs the setting of the .status.latestDigest field.
+DigestReflectionPolicy governs the setting of the .status.latestDigest field.
|
| |
@@ -302,7 +302,7 @@ ReflectionPolicy
(Optional)
- ReflectDigest governs the setting of the .status.latestDigest field.
+DigestReflectionPolicy governs the setting of the .status.latestDigest field.
|
diff --git a/internal/controller/imagepolicy_controller.go b/internal/controller/imagepolicy_controller.go
index d0300066..0713f3c9 100644
--- a/internal/controller/imagepolicy_controller.go
+++ b/internal/controller/imagepolicy_controller.go
@@ -109,10 +109,10 @@ type ImagePolicyReconciler struct {
kuberecorder.EventRecorder
helper.Metrics
- ControllerName string
- Database DatabaseReader
- ACLOptions acl.Options
- RegistryHelper registry.Helper
+ ControllerName string
+ Database DatabaseReader
+ ACLOptions acl.Options
+ AuthOptionsGetter registry.AuthOptionsGetter
patchOptions []patch.Option
}
@@ -386,7 +386,7 @@ func (r *ImagePolicyReconciler) fetchDigest(ctx context.Context, repo *imagev1.I
if err != nil {
return "", fmt.Errorf("failed parsing reference %q: %w", ref, err)
}
- opts, err := r.RegistryHelper.GetAuthOptions(ctx, *repo)
+ opts, err := r.AuthOptionsGetter(ctx, *repo)
if err != nil {
return "", fmt.Errorf("failed to configure authentication options: %w", err)
}
diff --git a/internal/controller/imagepolicy_controller_test.go b/internal/controller/imagepolicy_controller_test.go
index 17e2d3e6..188324bf 100644
--- a/internal/controller/imagepolicy_controller_test.go
+++ b/internal/controller/imagepolicy_controller_test.go
@@ -455,7 +455,7 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) {
EventRecorder: record.NewFakeRecorder(32),
Client: c,
Database: &mockDatabase{TagData: imageRepo.Status.LastScanResult.LatestTags},
- RegistryHelper: registry.NewDefaultHelper(c, login.ProviderOptions{
+ AuthOptionsGetter: registry.NewAuthOptionsGetter(c, login.ProviderOptions{
AwsAutoLogin: false,
AzureAutoLogin: false,
GcpAutoLogin: false,
diff --git a/internal/controller/imagerepository_controller.go b/internal/controller/imagerepository_controller.go
index f1156a34..9f36868b 100644
--- a/internal/controller/imagerepository_controller.go
+++ b/internal/controller/imagerepository_controller.go
@@ -107,7 +107,7 @@ type ImageRepositoryReconciler struct {
DatabaseReader
}
- RegistryHelper registry.Helper
+ AuthOptionsGetter registry.AuthOptionsGetter
patchOptions []patch.Option
}
@@ -252,7 +252,7 @@ func (r *ImageRepositoryReconciler) reconcile(ctx context.Context, sp *patch.Ser
}
conditions.Delete(obj, meta.StalledCondition)
- opts, err := r.RegistryHelper.GetAuthOptions(ctx, *obj)
+ opts, err := r.AuthOptionsGetter(ctx, *obj)
if err != nil {
e := fmt.Errorf("failed to configure authentication options: %w", err)
conditions.MarkFalse(obj, meta.ReadyCondition, imagev1.AuthenticationFailedReason, e.Error())
diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go
index cf4dfd64..ed62b34e 100644
--- a/internal/controller/suite_test.go
+++ b/internal/controller/suite_test.go
@@ -87,13 +87,13 @@ func TestMain(m *testing.M) {
panic(fmt.Sprintf("Failed to create new Badger database: %v", err))
}
- regHelper := registry.NewDefaultHelper(testEnv, login.ProviderOptions{})
+ optGetter := registry.NewAuthOptionsGetter(testEnv, login.ProviderOptions{})
if err = (&ImageRepositoryReconciler{
- Client: testEnv,
- Database: database.NewBadgerDatabase(testBadgerDB),
- EventRecorder: record.NewFakeRecorder(256),
- RegistryHelper: regHelper,
+ Client: testEnv,
+ Database: database.NewBadgerDatabase(testBadgerDB),
+ EventRecorder: record.NewFakeRecorder(256),
+ AuthOptionsGetter: optGetter,
}).SetupWithManager(testEnv, ImageRepositoryReconcilerOptions{
RateLimiter: controller.GetDefaultRateLimiter(),
}); err != nil {
@@ -101,10 +101,10 @@ func TestMain(m *testing.M) {
}
if err = (&ImagePolicyReconciler{
- Client: testEnv,
- Database: database.NewBadgerDatabase(testBadgerDB),
- EventRecorder: record.NewFakeRecorder(256),
- RegistryHelper: regHelper,
+ Client: testEnv,
+ Database: database.NewBadgerDatabase(testBadgerDB),
+ EventRecorder: record.NewFakeRecorder(256),
+ AuthOptionsGetter: optGetter,
}).SetupWithManager(testEnv, ImagePolicyReconcilerOptions{
RateLimiter: controller.GetDefaultRateLimiter(),
}); err != nil {
diff --git a/internal/registry/helper.go b/internal/registry/helper.go
index d3153946..3e5de8b1 100644
--- a/internal/registry/helper.go
+++ b/internal/registry/helper.go
@@ -17,35 +17,12 @@ limitations under the License.
package registry
import (
- "context"
"fmt"
"strings"
- imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
- "github.com/fluxcd/pkg/oci/auth/login"
"github.com/google/go-containerregistry/pkg/name"
- "github.com/google/go-containerregistry/pkg/v1/remote"
- "sigs.k8s.io/controller-runtime/pkg/client"
)
-type Helper interface {
- GetAuthOptions(ctx context.Context, obj imagev1.ImageRepository) ([]remote.Option, error)
-}
-
-type DefaultHelper struct {
- k8sClient client.Client
- DeprecatedLoginOpts login.ProviderOptions
-}
-
-var _ Helper = DefaultHelper{}
-
-func NewDefaultHelper(c client.Client, deprecatedLoginOpts login.ProviderOptions) DefaultHelper {
- return DefaultHelper{
- k8sClient: c,
- DeprecatedLoginOpts: deprecatedLoginOpts,
- }
-}
-
// ParseImageReference parses the given URL into a container registry repository
// reference.
func ParseImageReference(url string) (name.Reference, error) {
diff --git a/internal/registry/options.go b/internal/registry/options.go
index add04bcf..f7f930bb 100644
--- a/internal/registry/options.go
+++ b/internal/registry/options.go
@@ -29,123 +29,140 @@ import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
"github.com/fluxcd/image-reflector-controller/internal/secret"
)
-// GetAuthOptions returns authentication options required to scan a repository.
-func (h DefaultHelper) GetAuthOptions(ctx context.Context, obj imagev1.ImageRepository) ([]remote.Option, error) {
- timeout := obj.GetTimeout()
- ctx, cancel := context.WithTimeout(ctx, timeout)
- defer cancel()
-
- // Configure authentication strategy to access the registry.
- var options []remote.Option
- var authSecret corev1.Secret
- var auth authn.Authenticator
- var authErr error
-
- ref, err := ParseImageReference(obj.Spec.Image)
- if err != nil {
- return nil, fmt.Errorf("failed parsing image reference: %w", err)
- }
-
- if obj.Spec.SecretRef != nil {
- if err := h.k8sClient.Get(ctx, types.NamespacedName{
- Namespace: obj.GetNamespace(),
- Name: obj.Spec.SecretRef.Name,
- }, &authSecret); err != nil {
- return nil, err
- }
- auth, authErr = secret.AuthFromSecret(authSecret, ref)
- } else {
- // Build login provider options and use it to attempt registry login.
- opts := login.ProviderOptions{}
- switch obj.GetProvider() {
- case "aws":
- opts.AwsAutoLogin = true
- case "azure":
- opts.AzureAutoLogin = true
- case "gcp":
- opts.GcpAutoLogin = true
- default:
- opts = h.DeprecatedLoginOpts
- }
- auth, authErr = login.NewManager().Login(ctx, obj.Spec.Image, ref, opts)
- }
- if authErr != nil {
- // If it's not unconfigured provider error, abort reconciliation.
- // Continue reconciliation if it's unconfigured providers for scanning
- // public repositories.
- if !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
- return nil, authErr
+// AuthOptionsGetter is a function to extract information out of an ImageRepository and create
+// options from it that can be used to interact with an OCI registry.
+type AuthOptionsGetter func(ctx context.Context, obj imagev1.ImageRepository) ([]remote.Option, error)
+
+// NewAuthOptionsGetter returns an AuthOptionsGetter function that builds a slice of options from an
+// ImageRepository by looking up references to Secrets etc. on the Kubernetes cluster using the provided
+// client interface. If no external authentication provider is configured on the ImageRepository, the given
+// ProviderOptions are used for authentication. Options are extracted from the following ImageRepository spec
+// fields:
+//
+// - spec.image
+// - spec.secretRef
+// - spec.provider
+// - spec.certSecretRef
+// - spec.serviceAccountName
+func NewAuthOptionsGetter(c client.Client, deprecatedLoginOpts login.ProviderOptions) AuthOptionsGetter {
+ return func(ctx context.Context, obj imagev1.ImageRepository) ([]remote.Option, error) {
+ timeout := obj.GetTimeout()
+ ctx, cancel := context.WithTimeout(ctx, timeout)
+ defer cancel()
+
+ // Configure authentication strategy to access the registry.
+ var options []remote.Option
+ var authSecret corev1.Secret
+ var auth authn.Authenticator
+ var authErr error
+
+ ref, err := ParseImageReference(obj.Spec.Image)
+ if err != nil {
+ return nil, fmt.Errorf("failed parsing image reference: %w", err)
}
- }
- if auth != nil {
- options = append(options, remote.WithAuth(auth))
- }
- // Load any provided certificate.
- if obj.Spec.CertSecretRef != nil {
- var certSecret corev1.Secret
- if obj.Spec.SecretRef != nil && obj.Spec.SecretRef.Name == obj.Spec.CertSecretRef.Name {
- certSecret = authSecret
- } else {
- if err := h.k8sClient.Get(ctx, types.NamespacedName{
+ if obj.Spec.SecretRef != nil {
+ if err := c.Get(ctx, types.NamespacedName{
Namespace: obj.GetNamespace(),
- Name: obj.Spec.CertSecretRef.Name,
- }, &certSecret); err != nil {
+ Name: obj.Spec.SecretRef.Name,
+ }, &authSecret); err != nil {
return nil, err
}
- }
-
- tr, err := secret.TransportFromKubeTLSSecret(&certSecret)
- if err != nil {
- return nil, err
- }
- if tr.TLSClientConfig == nil {
- tr, err = secret.TransportFromSecret(&certSecret)
- if err != nil {
- return nil, err
+ auth, authErr = secret.AuthFromSecret(authSecret, ref)
+ } else {
+ // Build login provider options and use it to attempt registry login.
+ opts := login.ProviderOptions{}
+ switch obj.GetProvider() {
+ case "aws":
+ opts.AwsAutoLogin = true
+ case "azure":
+ opts.AzureAutoLogin = true
+ case "gcp":
+ opts.GcpAutoLogin = true
+ default:
+ opts = deprecatedLoginOpts
}
- if tr.TLSClientConfig != nil {
- ctrl.LoggerFrom(ctx).
- Info("warning: specifying TLS auth data via `certFile`/`keyFile`/`caFile` is deprecated, please use `tls.crt`/`tls.key`/`ca.crt` instead")
+ auth, authErr = login.NewManager().Login(ctx, obj.Spec.Image, ref, opts)
+ }
+ if authErr != nil {
+ // If it's not unconfigured provider error, abort reconciliation.
+ // Continue reconciliation if it's unconfigured providers for scanning
+ // public repositories.
+ if !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
+ return nil, authErr
}
}
- options = append(options, remote.WithTransport(tr))
- }
-
- if obj.Spec.ServiceAccountName != "" {
- serviceAccount := corev1.ServiceAccount{}
- // Lookup service account
- if err := h.k8sClient.Get(ctx, types.NamespacedName{
- Namespace: obj.GetNamespace(),
- Name: obj.Spec.ServiceAccountName,
- }, &serviceAccount); err != nil {
- return nil, err
+ if auth != nil {
+ options = append(options, remote.WithAuth(auth))
}
- if len(serviceAccount.ImagePullSecrets) > 0 {
- imagePullSecrets := make([]corev1.Secret, len(serviceAccount.ImagePullSecrets))
- for i, ips := range serviceAccount.ImagePullSecrets {
- var saAuthSecret corev1.Secret
- if err := h.k8sClient.Get(ctx, types.NamespacedName{
+ // Load any provided certificate.
+ if obj.Spec.CertSecretRef != nil {
+ var certSecret corev1.Secret
+ if obj.Spec.SecretRef != nil && obj.Spec.SecretRef.Name == obj.Spec.CertSecretRef.Name {
+ certSecret = authSecret
+ } else {
+ if err := c.Get(ctx, types.NamespacedName{
Namespace: obj.GetNamespace(),
- Name: ips.Name,
- }, &saAuthSecret); err != nil {
+ Name: obj.Spec.CertSecretRef.Name,
+ }, &certSecret); err != nil {
return nil, err
}
- imagePullSecrets[i] = saAuthSecret
}
- keychain, err := k8schain.NewFromPullSecrets(ctx, imagePullSecrets)
+
+ tr, err := secret.TransportFromKubeTLSSecret(&certSecret)
if err != nil {
return nil, err
}
- options = append(options, remote.WithAuthFromKeychain(keychain))
+ if tr.TLSClientConfig == nil {
+ tr, err = secret.TransportFromSecret(&certSecret)
+ if err != nil {
+ return nil, err
+ }
+ if tr.TLSClientConfig != nil {
+ ctrl.LoggerFrom(ctx).
+ Info("warning: specifying TLS auth data via `certFile`/`keyFile`/`caFile` is deprecated, please use `tls.crt`/`tls.key`/`ca.crt` instead")
+ }
+ }
+ options = append(options, remote.WithTransport(tr))
}
- }
- return options, nil
+ if obj.Spec.ServiceAccountName != "" {
+ serviceAccount := corev1.ServiceAccount{}
+ // Lookup service account
+ if err := c.Get(ctx, types.NamespacedName{
+ Namespace: obj.GetNamespace(),
+ Name: obj.Spec.ServiceAccountName,
+ }, &serviceAccount); err != nil {
+ return nil, err
+ }
+
+ if len(serviceAccount.ImagePullSecrets) > 0 {
+ imagePullSecrets := make([]corev1.Secret, len(serviceAccount.ImagePullSecrets))
+ for i, ips := range serviceAccount.ImagePullSecrets {
+ var saAuthSecret corev1.Secret
+ if err := c.Get(ctx, types.NamespacedName{
+ Namespace: obj.GetNamespace(),
+ Name: ips.Name,
+ }, &saAuthSecret); err != nil {
+ return nil, err
+ }
+ imagePullSecrets[i] = saAuthSecret
+ }
+ keychain, err := k8schain.NewFromPullSecrets(ctx, imagePullSecrets)
+ if err != nil {
+ return nil, err
+ }
+ options = append(options, remote.WithAuthFromKeychain(keychain))
+ }
+ }
+
+ return options, nil
+ }
}
diff --git a/internal/registry/helper_test.go b/internal/registry/options_test.go
similarity index 97%
rename from internal/registry/helper_test.go
rename to internal/registry/options_test.go
index b401a77e..18f1e3e1 100644
--- a/internal/registry/helper_test.go
+++ b/internal/registry/options_test.go
@@ -34,7 +34,7 @@ import (
"github.com/fluxcd/image-reflector-controller/internal/test"
)
-func TestDefaultHelperAuthOptions(t *testing.T) {
+func TestNewAuthOptionsGetter(t *testing.T) {
testImg := "example.com/foo/bar"
testSecretName := "test-secret"
testTLSSecretName := "test-tls-secret"
@@ -232,9 +232,9 @@ func TestDefaultHelperAuthOptions(t *testing.T) {
k8sClient := fake.NewClientBuilder().
WithObjects(tt.k8sObjs...).
Build()
- h := registry.NewDefaultHelper(k8sClient, login.ProviderOptions{})
+ getter := registry.NewAuthOptionsGetter(k8sClient, login.ProviderOptions{})
- opts, err := h.GetAuthOptions(context.Background(), tt.repo)
+ opts, err := getter(context.Background(), tt.repo)
if tt.expectErr {
g.Expect(err).To(HaveOccurred())
} else {
diff --git a/main.go b/main.go
index 10b834a9..3f2272d3 100644
--- a/main.go
+++ b/main.go
@@ -211,15 +211,15 @@ func main() {
AzureAutoLogin: azureAutoLogin,
GcpAutoLogin: gcpAutoLogin,
}
- registryHelper := registry.NewDefaultHelper(mgr.GetClient(), deprecatedLoginOptions)
+ authOptionsGetter := registry.NewAuthOptionsGetter(mgr.GetClient(), deprecatedLoginOptions)
if err := (&controller.ImageRepositoryReconciler{
- Client: mgr.GetClient(),
- EventRecorder: eventRecorder,
- Metrics: metricsH,
- Database: db,
- ControllerName: controllerName,
- RegistryHelper: registryHelper,
+ Client: mgr.GetClient(),
+ EventRecorder: eventRecorder,
+ Metrics: metricsH,
+ Database: db,
+ ControllerName: controllerName,
+ AuthOptionsGetter: authOptionsGetter,
}).SetupWithManager(mgr, controller.ImageRepositoryReconcilerOptions{
RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
}); err != nil {
@@ -227,13 +227,13 @@ func main() {
os.Exit(1)
}
if err := (&controller.ImagePolicyReconciler{
- Client: mgr.GetClient(),
- EventRecorder: eventRecorder,
- Metrics: metricsH,
- Database: db,
- ACLOptions: aclOptions,
- ControllerName: controllerName,
- RegistryHelper: registryHelper,
+ Client: mgr.GetClient(),
+ EventRecorder: eventRecorder,
+ Metrics: metricsH,
+ Database: db,
+ ACLOptions: aclOptions,
+ ControllerName: controllerName,
+ AuthOptionsGetter: authOptionsGetter,
}).SetupWithManager(mgr, controller.ImagePolicyReconcilerOptions{
RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
}); err != nil {
From efefbd5d43575fb39545fe13a130a666a3809917 Mon Sep 17 00:00:00 2001
From: Max Jonas Werner
Date: Tue, 5 Sep 2023 15:29:04 +0200
Subject: [PATCH 09/17] Better error message
.spec.image has no relevance in the given package, anymore.
Signed-off-by: Max Jonas Werner
---
internal/registry/helper.go | 14 +++++++-------
internal/registry/options.go | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/internal/registry/helper.go b/internal/registry/helper.go
index 3e5de8b1..0a163ceb 100644
--- a/internal/registry/helper.go
+++ b/internal/registry/helper.go
@@ -23,19 +23,19 @@ import (
"github.com/google/go-containerregistry/pkg/name"
)
-// ParseImageReference parses the given URL into a container registry repository
-// reference.
-func ParseImageReference(url string) (name.Reference, error) {
- if s := strings.Split(url, "://"); len(s) > 1 {
- return nil, fmt.Errorf(".spec.image value should not start with URL scheme; remove '%s://'", s[0])
+// ParseImageReference parses the given reference string into a container
+// registry repository reference.
+func ParseImageReference(refs string) (name.Reference, error) {
+ if s := strings.Split(refs, "://"); len(s) > 1 {
+ return nil, fmt.Errorf("image reference value should not include URL scheme; remove '%s://'", s[0])
}
- ref, err := name.ParseReference(url)
+ ref, err := name.ParseReference(refs)
if err != nil {
return nil, err
}
- imageName := strings.TrimPrefix(url, ref.Context().RegistryStr())
+ imageName := strings.TrimPrefix(refs, ref.Context().RegistryStr())
if s := strings.Split(imageName, ":"); len(s) > 1 {
return nil, fmt.Errorf(".spec.image value should not contain a tag; remove ':%s'", s[1])
}
diff --git a/internal/registry/options.go b/internal/registry/options.go
index f7f930bb..4657e09d 100644
--- a/internal/registry/options.go
+++ b/internal/registry/options.go
@@ -64,7 +64,7 @@ func NewAuthOptionsGetter(c client.Client, deprecatedLoginOpts login.ProviderOpt
ref, err := ParseImageReference(obj.Spec.Image)
if err != nil {
- return nil, fmt.Errorf("failed parsing image reference: %w", err)
+ return nil, fmt.Errorf("failed parsing image reference %q: %w", obj.Spec.Image, err)
}
if obj.Spec.SecretRef != nil {
From 6e3a989a59e6693b84374d7be857867132ede1c5 Mon Sep 17 00:00:00 2001
From: Max Jonas Werner
Date: Tue, 5 Sep 2023 15:48:27 +0200
Subject: [PATCH 10/17] Fix test
Signed-off-by: Max Jonas Werner
---
internal/controller/scan_test.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/internal/controller/scan_test.go b/internal/controller/scan_test.go
index f7c999a3..43f95df7 100644
--- a/internal/controller/scan_test.go
+++ b/internal/controller/scan_test.go
@@ -367,7 +367,7 @@ func TestImageRepositoryReconciler_imageAttribute_schemePrefix(t *testing.T) {
ready = apimeta.FindStatusCondition(repo.GetConditions(), meta.ReadyCondition)
return ready != nil && ready.Reason == imagev1.ImageURLInvalidReason
}, timeout, interval).Should(BeTrue())
- g.Expect(ready.Message).To(ContainSubstring("should not start with URL scheme"))
+ g.Expect(ready.Message).To(ContainSubstring("should not include URL scheme"))
// Check if the object status is valid.
condns := &conditionscheck.Conditions{NegativePolarity: imageRepositoryNegativeConditions}
From a3e050f856c1471d7fcf54986c75a9a809e62092 Mon Sep 17 00:00:00 2001
From: Max Jonas Werner
Date: Wed, 6 Sep 2023 16:49:55 +0200
Subject: [PATCH 11/17] Remove noop statements from test
These must have been leftovers from previous iterations of this test.
Signed-off-by: Max Jonas Werner
---
internal/controller/imagepolicy_controller_test.go | 14 +-------------
1 file changed, 1 insertion(+), 13 deletions(-)
diff --git a/internal/controller/imagepolicy_controller_test.go b/internal/controller/imagepolicy_controller_test.go
index 188324bf..726db973 100644
--- a/internal/controller/imagepolicy_controller_test.go
+++ b/internal/controller/imagepolicy_controller_test.go
@@ -422,7 +422,7 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Namespace: ns.Name,
Name: "digref-test",
- Finalizers: []string{imagev1.ImagePolicyFinalizer},
+ Finalizers: []string{imagev1.ImageFinalizer},
},
Spec: imagev1.ImagePolicySpec{
ImageRepositoryRef: meta.NamespacedObjectReference{
@@ -481,22 +481,10 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) {
// Now, change the policy (if the test desires it) and overwrite the existing latest tag with a new image
- defer func() {
- g.Expect(
- c.Update(context.Background(), imagePol),
- ).To(Succeed(), "failed resetting image policy to original values")
- }()
-
if tt.refPolicy1stPass != tt.refPolicy2ndPass {
- defer func(p *imagev1.ReflectionPolicy) {
- imagePol.Spec.DigestReflectionPolicy = p
- }(imagePol.Spec.DigestReflectionPolicy)
imagePol.Spec.DigestReflectionPolicy = tt.refPolicy2ndPass
}
if tt.semVerPolicy2ndPass != "" {
- defer func(s string) {
- imagePol.Spec.Policy.SemVer.Range = s
- }(imagePol.Spec.Policy.SemVer.Range)
imagePol.Spec.Policy.SemVer.Range = tt.semVerPolicy2ndPass
}
From af0fa03fdfafa1f8dec00e149c60396b27c8f5b5 Mon Sep 17 00:00:00 2001
From: Max Jonas Werner
Date: Thu, 7 Sep 2023 15:38:55 +0200
Subject: [PATCH 12/17] Some small API changes to ImagePolicy
1. Default digestReflectionPolicy to "Never" and add a getter. With
the getter method we will never encounter an empty policy even if
defaulting hasn't taken place.
2. Make status.latestRef a pointer to align with
status.observedPreviousRef. Having both fields be pointers makes it
easier to use them in code so we only have to compare to nil and
not the zero value.
Signed-off-by: Max Jonas Werner
---
api/v1beta2/imagepolicy_types.go | 17 +++++----
api/v1beta2/zz_generated.deepcopy.go | 11 +++---
...image.toolkit.fluxcd.io_imagepolicies.yaml | 9 ++---
docs/api/v1beta2/image-reflector.md | 2 --
internal/controller/imagepolicy_controller.go | 9 ++---
.../controller/imagepolicy_controller_test.go | 36 +++++++++----------
internal/controller/policy_test.go | 6 ++--
7 files changed, 44 insertions(+), 46 deletions(-)
diff --git a/api/v1beta2/imagepolicy_types.go b/api/v1beta2/imagepolicy_types.go
index be391e9a..2bb6cc1a 100644
--- a/api/v1beta2/imagepolicy_types.go
+++ b/api/v1beta2/imagepolicy_types.go
@@ -43,8 +43,8 @@ type ImagePolicySpec struct {
// +optional
FilterTags *TagFilter `json:"filterTags,omitempty"`
// DigestReflectionPolicy governs the setting of the `.status.latestDigest` field.
- // +optional
- DigestReflectionPolicy *ReflectionPolicy `json:"digestReflectionPolicy,omitempty"`
+ // +kubebuilder:default:=Never
+ DigestReflectionPolicy ReflectionPolicy `json:"digestReflectionPolicy,omitempty"`
}
// ReflectionPolicy describes a policy for if/when to reflect a value from the registry in a certain resource field.
@@ -153,7 +153,7 @@ type ImagePolicyStatus struct {
// LatestRef gives the first in the list of images scanned by
// the image repository, when filtered and ordered according
// to the policy.
- LatestRef ImageRef `json:"latestRef,omitempty"`
+ LatestRef *ImageRef `json:"latestRef,omitempty"`
// ObservedPreviousRef is the observed previous LatestRef. It is used
// to keep track of the previous and current images.
// +optional
@@ -177,9 +177,7 @@ func (p *ImagePolicy) SetConditions(conditions []metav1.Condition) {
// +kubebuilder:storageversion
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
-// +kubebuilder:printcolumn:name="LatestImage",type=string,JSONPath=`.status.latestRef.image`
-// +kubebuilder:printcolumn:name="LatestTag",type=string,JSONPath=`.status.latestRef.tag`
-// +kubebuilder:printcolumn:name="LatestDigest",type=string,JSONPath=`.status.latestRef.digest`
+// +kubebuilder:printcolumn:name="LatestImage",type=string,JSONPath=`.status.latestImage`
// ImagePolicy is the Schema for the imagepolicies API
type ImagePolicy struct {
@@ -191,6 +189,13 @@ type ImagePolicy struct {
Status ImagePolicyStatus `json:"status,omitempty"`
}
+func (p ImagePolicy) GetDigestReflectionPolicy() ReflectionPolicy {
+ if p.Spec.DigestReflectionPolicy != "" {
+ return p.Spec.DigestReflectionPolicy
+ }
+ return ReflectNever
+}
+
//+kubebuilder:object:root=true
// ImagePolicyList contains a list of ImagePolicy
diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go
index 34ec2ffb..5ff87a85 100644
--- a/api/v1beta2/zz_generated.deepcopy.go
+++ b/api/v1beta2/zz_generated.deepcopy.go
@@ -142,11 +142,6 @@ func (in *ImagePolicySpec) DeepCopyInto(out *ImagePolicySpec) {
*out = new(TagFilter)
**out = **in
}
- if in.DigestReflectionPolicy != nil {
- in, out := &in.DigestReflectionPolicy, &out.DigestReflectionPolicy
- *out = new(ReflectionPolicy)
- **out = **in
- }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePolicySpec.
@@ -162,7 +157,11 @@ func (in *ImagePolicySpec) DeepCopy() *ImagePolicySpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImagePolicyStatus) DeepCopyInto(out *ImagePolicyStatus) {
*out = *in
- out.LatestRef = in.LatestRef
+ if in.LatestRef != nil {
+ in, out := &in.LatestRef, &out.LatestRef
+ *out = new(ImageRef)
+ **out = **in
+ }
if in.ObservedPreviousRef != nil {
in, out := &in.ObservedPreviousRef, &out.ObservedPreviousRef
*out = new(ImageRef)
diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml
index 7d4d704b..0d0e877a 100644
--- a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml
+++ b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml
@@ -206,15 +206,9 @@ spec:
subresources:
status: {}
- additionalPrinterColumns:
- - jsonPath: .status.latestRef.image
+ - jsonPath: .status.latestImage
name: LatestImage
type: string
- - jsonPath: .status.latestRef.tag
- name: LatestTag
- type: string
- - jsonPath: .status.latestRef.digest
- name: LatestDigest
- type: string
name: v1beta2
schema:
openAPIV3Schema:
@@ -237,6 +231,7 @@ spec:
ImagePolicy.
properties:
digestReflectionPolicy:
+ default: Never
description: DigestReflectionPolicy governs the setting of the `.status.latestDigest`
field.
enum:
diff --git a/docs/api/v1beta2/image-reflector.md b/docs/api/v1beta2/image-reflector.md
index 6af1ab27..1e3233a7 100644
--- a/docs/api/v1beta2/image-reflector.md
+++ b/docs/api/v1beta2/image-reflector.md
@@ -141,7 +141,6 @@ ReflectionPolicy
|
-(Optional)
DigestReflectionPolicy governs the setting of the .status.latestDigest field.
|
| | |