From 727aff4b2e0271eff5fd8bf2a76282d8ec5f7669 Mon Sep 17 00:00:00 2001 From: Max Jonas Werner Date: Fri, 5 May 2023 18:40:21 +0200 Subject: [PATCH 01/17] Store digest of latest image in ImagePolicy's status The new API field `.status.latestDigest` in the `ImagePolicy` kind stores the digest of the image referred to by the the `.status.latestImage` field. The setting of this field is governed by the newly introduced field `.spec.digestReflectionPolicy` which takes one of the values `Always` or `IfNotPresent`. See the updated documentation under `docs/spec/` for details. The new status field can be used to pin an image to an immutable descriptor rather than to a potentially moving tag, increasing the security of workloads deployed on a cluster. The goal is to make use of the digest in IAC so that manifests can be updated with the actual image digest. Signed-off-by: Max Jonas Werner --- api/v1beta2/imagepolicy_types.go | 20 ++ api/v1beta2/zz_generated.deepcopy.go | 5 + ...image.toolkit.fluxcd.io_imagepolicies.yaml | 11 + docs/api/v1beta2/image-reflector.md | 48 +++ docs/spec/v1beta2/imagepolicies.md | 41 ++- internal/controller/imagepolicy_controller.go | 52 +++- .../controller/imagepolicy_controller_test.go | 261 ++++++++++++++++ .../controller/imagerepository_controller.go | 148 +-------- .../imagerepository_controller_test.go | 276 +---------------- internal/controller/policy_test.go | 11 +- internal/controller/scan_test.go | 14 +- internal/controller/suite_test.go | 22 +- internal/registry/helper.go | 51 ++++ internal/registry/helper_test.go | 289 ++++++++++++++++++ internal/registry/options.go | 135 ++++++++ internal/test/registry.go | 18 +- internal/test/registry_test.go | 2 +- main.go | 15 +- 18 files changed, 966 insertions(+), 453 deletions(-) create mode 100644 internal/registry/helper.go create mode 100644 internal/registry/helper_test.go create mode 100644 internal/registry/options.go diff --git a/api/v1beta2/imagepolicy_types.go b/api/v1beta2/imagepolicy_types.go index ae14cd69..4205664c 100644 --- a/api/v1beta2/imagepolicy_types.go +++ b/api/v1beta2/imagepolicy_types.go @@ -42,8 +42,24 @@ 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 { @@ -107,6 +123,10 @@ 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 diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 29f2b40d..14399b9d 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -142,6 +142,11 @@ 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/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml index 74af4e42..52d54a8c 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml @@ -230,6 +230,13 @@ spec: description: ImagePolicySpec defines the parameters for calculating the ImagePolicy. properties: + digestReflectionPolicy: + description: ReflectDigest governs the setting of the `.status.latestDigest` + field. + enum: + - Always + - IfNotPresent + type: string 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 @@ -382,6 +389,10 @@ 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 by the image repository, when filtered and ordered according to diff --git a/docs/api/v1beta2/image-reflector.md b/docs/api/v1beta2/image-reflector.md index 576479fa..7dd8af52 100644 --- a/docs/api/v1beta2/image-reflector.md +++ b/docs/api/v1beta2/image-reflector.md @@ -131,6 +131,20 @@ rules. If no rules are provided, all the tags from the repository will be ordered and compared.

+ + +digestReflectionPolicy
+ + +ReflectionPolicy + + + + +(Optional) +

ReflectDigest governs the setting of the .status.latestDigest field.

+ + @@ -277,6 +291,20 @@ rules. If no rules are provided, all the tags from the repository will be ordered and compared.

+ + +digestReflectionPolicy
+ + +ReflectionPolicy + + + + +(Optional) +

ReflectDigest governs the setting of the .status.latestDigest field.

+ + @@ -313,6 +341,19 @@ the policy.

+latestDigest
+ +string + + + +(Optional) +

LatestDigest is the digest of the latest image stored in the +accompanying LatestImage field.

+ + + + observedPreviousImage
string @@ -872,6 +913,13 @@ would select 0.

+

ReflectionPolicy +(string alias)

+

+(Appears on: +ImagePolicySpec) +

+

ReflectionPolicy describes a policy for if/when to reflect a value from the registry in a certain resource field.

ScanResult

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:

-

image.toolkit.fluxcd.io/v1beta2

-

Package v1beta2 contains API types for the image API group, version -v1beta2. These types are concerned with reflecting metadata from +

image.toolkit.fluxcd.io/v1beta3

+

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:
    -

    AlphabeticalPolicy +

    AlphabeticalPolicy

    (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

    @@ -328,72 +328,118 @@ ReflectionPolicy -latestImage
    +latestRef
    -string + +ImageRef + -

    LatestImage gives the first in the list of images scanned by -the image repository, when filtered and ordered according to -the policy.

    +

    LatestRef gives the first in the list of images scanned by +the image repository, when filtered and ordered according +to the policy.

    -latestDigest
    +observedPreviousRef
    -string + +ImageRef + (Optional) -

    LatestDigest is the digest of the latest image stored in the -accompanying LatestImage field.

    +

    ObservedPreviousRef is the observed previous LatestRef. It is used +to keep track of the previous and current images.

    -observedPreviousImage
    +observedGeneration
    -string +int64 (Optional) -

    ObservedPreviousImage is the observed previous LatestImage. It is used -to keep track of the previous and current images.

    -observedGeneration
    +conditions
    -int64 + +[]Kubernetes meta/v1.Condition + (Optional) + + +
    + +

    ImageRef +

    +

    +(Appears on: +ImagePolicyStatus) +

    +

    ImageRef represents an image reference.

    +
    +
    + + + + + + + + + + + + + + + +
    FieldDescription
    -conditions
    +image
    - -[]Kubernetes meta/v1.Condition - +string + +
    +

    Name is the bare image’s name.

    +
    +tag
    + +string + +
    +

    Tag is the image’s tag.

    +
    +digest
    + +string
    (Optional) +

    Digest is the image’s digest.

    -

    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)

    @@ -972,11 +1018,11 @@ Kubernetes meta/v1.Time
    -

    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

    diff --git a/docs/spec/v1beta3/imagepolicies.md b/docs/spec/v1beta3/imagepolicies.md new file mode 100644 index 00000000..51c2b333 --- /dev/null +++ b/docs/spec/v1beta3/imagepolicies.md @@ -0,0 +1,495 @@ +# Image Policies + +The `ImagePolicies` API defines rules for selecting a "latest" image from +`ImageRepositories`. + +## Example + +The following is an example of an ImagePolicy. It queries the referred +ImageRepository for the image name of the repository, reads all the tags in +the repository and selects the latest tag based on the defined policy rules. + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImagePolicy +metadata: + name: podinfo + namespace: default +spec: + imageRepositoryRef: + name: podinfo + policy: + semver: + range: 5.1.x +``` + +In the above example: + +- An ImagePolicy named `podinfo` is created, indicated by the `.metadata.name` + field. +- The image-reflector-controller applies the latest tag selection policy every + time there's an update in the referred ImageRepository, indicated by the + `.spec.imageRepositoryRef.name` field. +- It fetches the canonical image name of the referred ImageRepository and reads + the scanned tags from the internal database for the image name. The read tags + 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` 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`. + +1. Apply the resource on the cluster: + +```sh +kubectl apply -f imagepolicy.yaml +``` + +2. Run `kubectl get imagepolicy` to see the ImagePolicy: + +```console +NAME LATESTIMAGE +podinfo ghcr.io/stefanprodan/podinfo:5.1.4 +``` + +3. Run `kubectl describe imagepolicy podinfo` to see the [Latest Image](#latest-image) +and [Conditions](#conditions) in the ImagePolicy's Status: + +```console +Status: + Conditions: + Last Transition Time: 2022-09-20T07:09:56Z + Message: Latest image tag for 'ghcr.io/stefanprodan/podinfo' resolved to 5.1.4 + Observed Generation: 1 + Reason: Succeeded + Status: True + Type: Ready + Latest Digest: sha256:2d9a00b3981628a533ff43352193b1838b0a4bf6b0033444286f563205e51a2c + Latest Image: ghcr.io/stefanprodan/podinfo:5.1.4 + Observed Generation: 1 +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Succeeded 7s (x3 over 8s) image-reflector-controller Latest image tag for 'ghcr.io/stefanprodan/podinfo' resolved to 5.1.4 +``` + +## Writing an ImagePolicy spec + +As with all other Kubernetes config, an ImagePolicy needs `apiVersion`, +`kind`, and `metadata` fields. The name of an ImagePolicy object must be a +valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names). + +An ImagePolicy also needs a +[`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status). + +### Image Repository Reference + +`.spec.imageRepositoryRef` is a required field that specifies the +ImageRepository for which the latest image has to be selected. The value must be +a namespaced object reference. For ImageRepository in the same namespace as the +ImagePolicy, no namespace needs to be provided. For ImageRepository in a +different namespace than the namespace of the ImagePolicy, namespace name has to +be provided. For example: + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImagePolicy +metadata: + name: podinfo + namespace: default +spec: + imageRepositoryRef: + name: podinfo + namespace: flux-system +... +``` + +The ImageRepository access is determied by its ACL for cross-namespace +reference. For more details on how to allow cross-namespace references see the +[ImageRepository docs](imagerepositories.md#access-from). + +### Policy + +`.spec.policy` is a required field that specifies how to choose a latest image +given the image metadata. There are three image policy choices: +- SemVer +- Alphabetical +- Numerical + +#### SemVer + +SemVer policy interprets all the tags as semver versions and chooses the highest +version available that fits the given +[semver constraints](https://github.com/Masterminds/semver#checking-version-constraints). +The constraint is set in the `.spec.policy.semver.range` field. + +Example of a SemVer image policy choice: + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImagePolicy +metadata: + name: podinfo +spec: + imageRepositoryRef: + name: podinfo + policy: + semver: + range: '>=1.0.0' +``` + +This will select the latest stable version tag. + +#### Alphabetical + +Alphabetical policy chooses the _last_ tag when all the tags are sorted +alphabetically (in either ascending or descending order). The sort order is set +in the `.spec.policy.alphabetical.order` field. The value could be `asc` for +ascending order or `desc` for descending order. The default value is `asc`. + +Example of an Alphabetical policy choice: + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImagePolicy +metadata: + name: podinfo +spec: + imageRepositoryRef: + name: podinfo + policy: + alphabetical: + order: asc +``` + +This will select the last tag when all the tags are sorted alphabetically in +ascending order. + +#### Numerical + +Numerical policy chooses the _last_ tag when all the tags are sorted numerically +(in either ascending or descending order). The sort order is set in the +`.spec.policy.numerical.order` field. The value could be `asc` for ascending +order or `desc` for descending order. The default value is `asc`. + +Example of a Numerical policy choice: + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImagePolicy +metadata: + name: podinfo +spec: + imageRepositoryRef: + name: podinfo + policy: + numerical: + order: asc +``` + +This will select the last tag when all the tags are sorted numerically in +ascending order. + +### Filter Tags + +`.spec.filterTags` is an optional field to specify a filter on the image tags +before they are considered by the policy rule. + +The filter pattern is a regular expression, set in the +`.spec.filterTags.pattern` field. Only tags that match the pattern are +considered by the policy rule. + +The `.spec.filterTags.extract` is an optional field used to extract a value from +the matching tags which is supplied to the policy rule instead of the original +tags. If unspecified, the tags that match the pattern will be used as they are. + +Example of selecting the latest release candidate (semver): + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImagePolicy +metadata: + name: podinfo +spec: + imageRepositoryRef: + name: podinfo + filterTags: + pattern: '.*-rc.*' + policy: + semver: + range: '^1.x-0' +``` + +Example of selecting the latest release tagged as `RELEASE.` +(alphabetical): + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImagePolicy +metadata: + name: minio +spec: + imageRepositoryRef: + name: minio + filterTags: + pattern: '^RELEASE\.(?P.*)Z$' + extract: '$timestamp' + policy: + alphabetical: + order: asc +``` + +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 + +ImagePolicy is reconciled automatically when the associated ImageRepository is +updated. Whenever ImageRepository gets updated, ImagePolicy will be triggered +and have the policy result based on the latest values of ImageRepository. To +manually tell the image-reflector-controller to reconcile an ImagePolicy, the +associated ImageRepository can be annotated with +`reconcile.fluxcd.io/requestedAt: `. +See [triggering a reconcile](imagerepositories.md#triggering-a-reconcile) for +more details about reconciling ImageRepository. + +### Waiting for `Ready` + +When a change is applied, it is possible to wait for the ImagePolicy to reach a +[ready state](#ready-imagepolicy) using `kubectl`: + +```sh +kubectl wait imagepolicy/ --for=condition=ready --timeout=1m +``` + +### Debugging an ImagePolicy + +There are several ways to gather information about an ImagePolicy for debugging +purposes. + +#### Describe the ImagePolicy + +Describing an ImagePolicy using `kubectl describe imagepolicy ` +displays the latest recorded information for the resource in the `Status` and +`Events` sections: + +```console +... +Status: + Conditions: + Last Transition Time: 2022-10-06T12:07:35Z + Message: accessing ImageRepository + Observed Generation: 1 + Reason: AccessingRepository + Status: True + Type: Reconciling + Last Transition Time: 2022-10-06T12:07:35Z + Message: failed to get the referred ImageRepository: referenced ImageRepository does not exist: ImageRepository.image.toolkit.fluxcd.io "podinfo" not found + Observed Generation: 1 + Reason: DependencyNotReady + Status: False + Type: Ready + Observed Generation: 1 +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Warning DependencyNotReady 2s (x4 over 5s) image-reflector-controller failed to get the referred ImageRepository: referenced ImageRepository does not exist: ImageRepository.image.toolkit.fluxcd.io "podinfo" not found +``` + +#### Trace emitted Events + +To view events for specific ImagePolicy(s), `kubectl events` can be used in +combination with `--for` to list the Events for specific objects. For example, +running + +```sh +kubectl events --for ImagePolicy/ +``` + +lists + +```console +LAST SEEN TYPE REASON OBJECT MESSAGE +4m44s Normal Succeeded imagepolicy/ Latest image tag for 'ghcr.io/stefanprodan/podinfo' resolved to 5.1.4 +95s Warning DependencyNotReady imagepolicy/ failed to get the referred ImageRepository: referenced ImageRepository does not exist: ImageRepository.image.toolkit.fluxcd.io "podinfo" not found +``` + +Besides being reported in Events, the reconciliation errors are also logged by +the controller. The Flux CLI offer commands for filtering the logs for a +specific ImagePolicy, e.g. +`flux logs --level=error --kind=ImagePolicy --name=`. + +## ImagePolicy Status + +### Latest Image + +The ImagePolicy reports the latest selected image from the ImageRepository tags in +`.status.latestImage` for the resource. + +Example: + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImagePolicy +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/v1beta3 +kind: ImagePolicy +metadata: + name: +status: + latestDigest: sha256:2d9a00b3981628a533ff43352193b1838b0a4bf6b0033444286f563205e51a2c +[...] +``` + +### Observed Previous Image + +The ImagePolicy reports the previously observed latest image in +`.status.observedPreviousImage` for the resource. This is used by the +ImagePolicy to determine an upgrade path of an ImagePolicy update. This field +is reset when the ImagePolicy fails due to some reason to be able to distinguish +between a failure recovery and a genuine latest image upgrade. + +Example: + +```yaml +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImagePolicy +metadata: + name: +status: + latestImage: ghcr.io/stefanprodan/podinfo:6.2.1 + observedPreviousImage: ghcr.io/stefanprodan/podinfo:5.1.4 +``` + +### Conditions + +An ImagePolicy enters various states during its lifecycle, reflected as +[Kubernetes Conditions][typical-status-properties]. +It can be [reconciling](#reconciling-imagepolicy) while reading the tags from +ImageRepository scan results, it can be [ready](#ready-imagepolicy), or it can +[fail during reconciliation](#failed-imagepolicy). + +The ImagePolicy API is compatible with the [kstatus specification][kstatus-spec], +and reports `Reconciling` and `Stalled` conditions where applicable to provide +better (timeout) support to solutions polling the ImagePolicy to become `Ready`. + +#### Reconciling ImagePolicy + +The image-reflector-controller marks an ImagePolicy as _reconciling_ when one of +the following is true: + +- The generation of the ImagePolicy is newer than the [Observed Generation](#observed-generation). +- The ImagePolicy is accessing the provided ImageRepository reference. +- The ImagePolicy is being applied to the tags read from an ImageRepository. + +When the ImagePolicy is "reconciling", the `Ready` Condition status becomes +`False`, and the controller adds a Condition with the following attributes to +the ImagePolicy's `.status.conditions`: + +- `type: Reconciling` +- `status: "True"` +- `reason: NewGeneration` | `reason:AccessingRepository` | `reason: ApplyingPolicy` + +It has a ["negative polarity"][typical-status-properties], and is only present +on the ImagePolicy while its status value is `"True"`. + +#### Ready ImagePolicy + +The image-reflector-controller marks an ImagePolicy as _ready_ when it has the +following characteristics: + +- The ImagePolicy reports a [Latest Image](#latest-image) +- The referenced ImageRepository is accessible and the internal tags database + contains the tags that ImagePolicy needs to apply the policy on. + +When the ImagePolicy is "ready", the controller sets a Condition with the +following attributes in the ImagePolicy's `.status.conditions`. + +- `type: Ready` +- `status: "True"` +- `reason: Succeeded` + +This `Ready` Condition will retain a status value of `"True"` until the +ImagePolicy is marked as [reconciling](#reconciling-imagepolicy), or e.g. a +[transient error](#failed-imagepolicy) occurs due to a temporary network issue. + +#### Failed ImagePolicy + +The image-reflector-controller may get stuck trying to apply a policy without +completing. This can occur due to some of the following factors: + +- The referenced ImageRepository is temporarily unavailable. +- The referenced ImageRepository does not exist. +- The referenced ImageRepository is not accessible in a different namespace. +- The ImagePolicy spec contains a generic misconfiguration. +- The ImagePolicy could not select the latest tag based on the given rules and + the available tags. +- A database related failure when reading or writing the scanned tags. + +When this happens, the controller sets the `Ready` condition status to `False` +wit the following reason: + +- `reason: Failure` | `reason: AccessDenied` | `reason: DependencyNotReady` + +While the ImagePolicy is in failing state, the controller will continue to +attempt to get the referenced ImageRepository for the resource and apply the +policy rules with an exponential backoff, until it succeeds and the ImagePolicy +is marked as [ready](#ready-imagepolicy). + +Note that an ImagePolicy can be [reconcilcing](#reconciling-imagepolicy) while +failing at the same time, for example due to a newly introduced configuration +issue in the ImagePolicy spec. + +### Observed Generation + +The image-reflector-controller reports an +[observed generation][typical-status-properties] in the ImagePolicy's +`.status.observedGeneration`. The observed generation is the latest +`.metadata.generation` which resulted in either a +[ready state](#ready-imagepolicy), or stalled due to error it can not +recover from without human intervention. + +[typical-status-properties]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties +[kstatus-spec]: https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus diff --git a/docs/spec/v1beta3/imagerepositories.md b/docs/spec/v1beta3/imagerepositories.md new file mode 100644 index 00000000..6666d107 --- /dev/null +++ b/docs/spec/v1beta3/imagerepositories.md @@ -0,0 +1,811 @@ +# Image Repositories + +The `ImageRepository` API defines a repository to scan and store a specific set +of tags in a database. + +## Example + +The following is an example of an ImageRepository. It scans the specified image +repository and stores the scanned tags in an internal database. + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImageRepository +metadata: + name: podinfo + namespace: default +spec: + image: stefanprodan/podinfo + interval: 1h + provider: generic +``` + +In the above example: + +- An ImageRepository named `podinfo` is created, indicated by the + `.metadata.name` field. +- The image-reflector-controller scans the image repository for tags every hour, + indicated by the `.spec.interval` field. +- The registry authentication is done using a generic provider, indicated by the + `.spec.provider` field and referenced using `.spec.secretRef`. No + authentication is attempted when secret reference is not provided for generic + provider. See [Provider](#provider) for more details related to registry + authentication. +- The canonical form of the image set in `.spec.image` is used to scan the + repository. The resolved canonical form of the image is reported in the + `.status.canonicalImageName` field. +- The result of the scan is reported in the `.status.lastScanResult` field. + +This example can be run by saving the manifest into `imagerepository.yaml`. + +1. Apply the resource on the cluster: + +```sh +kubectl apply -f imagerepository.yaml +``` + +2. Run `kubectl get imagerepository` to see the ImageRepository: + +```console +NAME LAST SCAN TAGS +podinfo 2022-09-15T22:34:05Z 211 +``` + +3. Run `kubectl describe imagerepository podinfo` to see the [Last Scan Result](#last-scan-result) +and [Conditions](#conditions) in the ImageRepository's Status: + +```console + +... +Status: + Canonical Image Name: index.docker.io/stefanprodan/podinfo + Conditions: + Last Transition Time: 2022-09-15T22:38:42Z + Message: successful scan, found 211 tags + Observed Generation: 1 + Reason: Succeeded + Status: True + Type: Ready + Last Scan Result: + Latest Tags: + latest + 6.2.0 + 6.1.8 + 6.1.7 + 6.1.6 + 6.1.5 + 6.1.4 + 6.1.3 + 6.1.2 + 6.1.1 + Scan Time: 2022-09-15T22:38:42Z + Tag Count: 211 + Observed Exclusion List: + ^.*\.sig$ + Observed Generation: 1 +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Succeeded 17s image-reflector-controller successful scan, found 211 tags +``` + +## Writing an ImageRepository spec + +As with all other Kubernetes config, an ImageRepository needs `apiVersion`, +`kind`, and `metadata` fields. The name of an ImageRepository object must be a +valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names). + +An ImageRepository also needs a +[`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status). + +### Image + +`.spec.image` is a required field that specifies the address of an image +repository without any scheme prefix, e.g. `fluxcd/image-reflector-controller`. +This image is converted to its canonical form by the controller before scanning. +The canonical form of the image is reflected in `.status.canonicalImageName`. + +### Interval + +`.spec.interval` is a required field that specifies the interval at which the +Image repository must be scanned. + +After successfully reconciling the object, the image-reflector-controller +requeues it for inspection after the specified interval. The value must be in a +[Go recognized duration string format](https://pkg.go.dev/time#ParseDuration), +e.g. `10m0s` to reconcile the object every 10 minutes. + +If the `.metadata.generation` of a resource changes (due to e.g. a change to +the spec), this is handled instantly outside the interval window. + +### Timeout + +`.spec.timeout` is an optional field to specify a timeout for various operations +during the reconciliation like fetching the referred secrets, scanning the +repository, etc. The value must be in a +[Go recognized duration string format](https://pkg.go.dev/time#ParseDuration), +e.g. `1m30s` for a timeout of one minute and thirty seconds. The default value +is the value of `.spec.interval`. + +### Secret reference + +`.spec.secretRef.name` is an optional field to specify a name reference to a +Secret in the same namespace as the ImageRepository, containing authentication +credentials for the Image repository. The secret is expected to be in the same +format as the [docker config secrets](https://kubernetes.io/docs/concepts/configuration/secret/#docker-config-secrets), usually created by `kubectl create secret +docker-registry`. + +Example of using secret reference in an ImageRepository: + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImageRepository +metadata: + name: podinfo + namespace: default +spec: + image: stefanprodan/podinfo + interval: 1h + secretRef: + name: regcred +--- +apiVersion: v1 +kind: Secret +metadata: + name: regcred + namespace: default +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsidXNlcm5hbWUiOiJmb28iLCJwYXNzd29yZCI6ImJhciIsImF1dGgiOiJabTl2T21KaGNnPT0ifX19 +``` + +For a publicly accessible image repository, there's no need to provide a secret +reference. + +### ServiceAccount name + +`.spec.serviceAccountName` is an optional field to specify a name reference to a +ServiceAccount in the same namespace as the ImageRepository, with an image pull +secret attached to it. For detailed instructions about attaching an image pull +secret to a ServiceAccount, see [Add image pull secret to service account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-image-pull-secret-to-service-account). + +### Certificate secret reference + +`.spec.certSecretRef` is an optional field to specify a name reference to a +Secret in the same namespace as the ImageRepository containing TLS certificate +data. This is for two separate purposes: +- to provide a client certificate and private key, if you use a certificate to authenticate with the image registry; and, +- to provide a CA certificate, if the registry uses a self-signed certificate + +These will often go together in case of self-hosted image registry. All the +files in the secret are expected to be [PEM-encoded][pem-encoding]. This is an +ASCII format for certificates and keys; `openssl` and such tools typically +provide an option for PEM output. + +Assuming that a certificate file and private key are in files `client.crt` and +`client.key` respectively, a secret can be created with `kubectl`: + +```sh +kubectl create secret generic tls-certs \ + --from-file=certFile=client.crt \ + --from-file=keyFile=client.key +``` + +An [encrypted secret](sops-guide) can also be used; the important bit is that +the data keys in the secret are `certFile` and `keyFile`. + +In case of a CA certificate for the client to use, the data key for it is +`caFile`. Adapting the previous example, if the certificate is in the file +`ca.crt`, and the client certificate and key are as before, the whole command +would be: + +```sh +kubectl create secret generic tls-certs \ + --from-file=certFile=client.crt \ + --from-file=keyFile=client.key \ + --from-file=caFile=ca.crt +``` + +### Suspend + +`.spec.suspend` is an optional field to suspend the reconciliation of an +ImageRepository. When set to `true`, the controller will stop reconciling the +ImageRepository, and changes to the resource or image repository will not result +in new scan results. When the field is set to `false` or removed, it will +resume. + +### Access from + +`.spec.accessFrom` is an optional field to restrict cross-namespace access of +ImageRepositories. To grant access to an ImageRepository for policies in other +namespaces, the owner of the ImageRepository has to specify a list of label +selectors that match the namespace labels of the ImagePolicy objects. + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImageRepository +metadata: + name: app1 + namespace: apps +spec: + interval: 1h + image: docker.io/org/image + secretRef: + name: regcred + accessFrom: + namespaceSelectors: + - matchLabels: + kubernetes.io/metadata.name: flux-system +``` + +**Note:** The `kubernetes.io/metadata.name` label above is a readonly label +added by Kubernetes >= 1.21 automatically on namespaces. For older version of +Kubernetes, please set labels on the namespaces where the ImagePolicy exist. + +The above definition, allows ImagePolicy in the `flux-system` namespace to +reference the `app1` ImageRepository e.g.: + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImagePolicy +metadata: + name: app1 + namespace: flux-system +spec: + imageRepositoryRef: + name: app1 + namespace: apps + policy: + semver: + range: 1.0.x +``` + +To grant access to all namespaces, an empty `matchLabels` can be set: + +```yaml + accessFrom: + namespaceSelectors: + - matchLabels: {} +``` + +### Exclusion list + +`.spec.exclusionList` is an optional field to exclude certain tags in the image +scan result. It's a list of regular expression patterns with a default value of +`"^.*\\.sig$"` if it's not set. This default value is used to exclude all the +tags ending with `.sig`, since these are [Cosign](https://github.com/sigstore/cosign) +generated objects and not container images which can be deployed on a Kubernetes +cluster. + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImageRepository +metadata: + name: app1 + namespace: apps +spec: + interval: 1h + image: docker.io/org/image + exclusionList: + - "^.*\\.sig$" + - "1.0.2" + - "1.1.1|1.0.0" +``` + +### Provider + +`.spec.provider` is an optional field that allows specifying an OIDC provider +used for authentication purposes. + +Supported options are: + +- `generic` +- `aws` +- `azure` +- `gcp` + +The `generic` provider can be used for public repositories or when static +credentials are used for authentication, either with `.spec.secretRef` or +`.spec.serviceAccount`. If `.spec.provider` is not specified, it defaults to +`generic`. + +#### AWS + +The `aws` provider can be used to authenticate automatically using the EKS +worker node IAM role or IAM Role for Service Accounts (IRSA), and by extension +gain access to ECR. + +##### Worker Node IAM + +When the worker node IAM role has access to ECR, image-reflector-controller +running on it will also have access to ECR. Please take a look at this +[documentation](https://docs.aws.amazon.com/eks/latest/userguide/create-node-role.html) +for creating worker node IAM roles. + +##### IAM roles for service accounts(IRSA) + +When using IRSA to enable access to ECR, add the following patch to your +bootstrap repository, in the `flux-system/kustomization.yaml` file: + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - gotk-components.yaml + - gotk-sync.yaml +patches: + - patch: | + apiVersion: v1 + kind: ServiceAccount + metadata: + name: image-reflector-controller + annotations: + eks.amazonaws.com/role-arn: + target: + kind: ServiceAccount + name: image-reflector-controller +``` + +Note that you can attach the AWS managed policy `arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly` +to the IAM role when using IRSA and you have to configure the +`image-reflector-controller` to assume the IAM role. Please see +[documentation](https://docs.aws.amazon.com/eks/latest/userguide/associate-service-account-role.html). + +#### Azure + +The `azure` provider can be used to authenticate automatically using Workload +Identity, kubelet managed identity or Azure Active Directory pod-managed +identity (aad-pod-identity), and by extension gain access to ACR. + +##### Kubelet Identity + +When the kubelet managed identity has access to ACR, image-reflector-controller +running on it will also have access to ACR. + +##### Workload Identity + +When using workload identity to enable access to ACR, add the following patch to +properly annotate the image-reflector-controller pods and service account +in the `flux-system/kustomization.yaml` file: + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - gotk-components.yaml + - gotk-sync.yaml +patches: + - patch: |- + apiVersion: v1 + kind: ServiceAccount + metadata: + name: image-reflector-controller + namespace: flux-system + annotations: + azure.workload.identity/client-id: + labels: + azure.workload.identity/use: "true" + - patch: |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: image-reflector-controller + namespace: flux-system + labels: + azure.workload.identity/use: "true" + spec: + template: + metadata: + labels: + azure.workload.identity/use: "true" +``` + +To use workload identity on your cluster, you would have to install workload +in your cluster, create an identity that has `AcrPull` role to ACR and establish +azure federated identity between the identity and the image-reflector-controller +service account. Please, take a look at the +[Azure documentation for Workload identity](https://azure.github.io/azure-workload-identity/docs/quick-start.html). + +##### AAD Pod Identity + +When using aad-pod-identity to enable access to ACR, add the following patch to +your bootstrap repository, in the `flux-system/kustomization.yaml` file: + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - gotk-components.yaml + - gotk-sync.yaml +patches: + - patch: | + - op: add + path: /spec/template/metadata/labels/aadpodidbinding + value: + target: + kind: Deployment + name: image-reflector-controller +``` + +When using pod-managed identity on an AKS cluster, AAD Pod Identity +has to be used to give the `image-reflector-controller` pod access to the ACR. +To do this, you have to install `aad-pod-identity` on your cluster, create a +managed identity that has access to the container registry (this can also be the +Kubelet identity if it has `AcrPull` role assignment on the ACR), create an +`AzureIdentity` and `AzureIdentityBinding` that describe the managed identity +and then label the `image-reflector-controller` pods with the name of the +AzureIdentity as shown in the patch above. Please take a look at +[this guide](https://azure.github.io/aad-pod-identity/docs/) or +[this one](https://docs.microsoft.com/en-us/azure/aks/use-azure-ad-pod-identity) +to use AKS pod-managed identities add-on that is in preview. + +#### GCP + +The `gcp` provider can be used to authenticate automatically using OAuth scopes +or Workload Identity, and by extension gain access to GCR or Artifact Registry. + +##### Access scopes + +When the GKE nodes have the appropriate OAuth scope for accessing GCR and +Artifact Registry, image-reflector-controller running on it will also have +access to them. + +##### Workload Identity + +When using Workload Identity to enable access to GCR or Artifact Registry, add +the following patch to your bootstrap repository, in the +`flux-system/kustomization.yaml` file: + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - gotk-components.yaml + - gotk-sync.yaml +patches: + - patch: | + apiVersion: v1 + kind: ServiceAccount + metadata: + name: image-reflector-controller + annotations: + iam.gke.io/gcp-service-account: + target: + kind: ServiceAccount + name: image-reflector-controller +``` + +The Artifact Registry service uses the permission `artifactregistry.repositories.downloadArtifacts` +that is located under the Artifact Registry Reader role. If you are using +Google Container Registry service, the needed permission is instead `storage.objects.list` +which can be bound as part of the Container Registry Service Agent role. +Take a look at [this guide](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) +for more information about setting up GKE Workload Identity. + +#### Authentication on other platforms + +For other platforms that link service permissions to service accounts, secret +can be created using tooling for that platform, rather than directly with +`kubectl create secret`. There is advice specific to some platforms in [the +image automation guide][image-auto-provider-secrets]. + +## Working with ImageRepositories + +### Triggering a reconcile + +To manually tell the image-reflector-controller to reconcile an ImageRepository +outside the [specified interval window](#interval), an ImageRepository can be +annotated with `reconcile.fluxcd.io/requestedAt: `. Annotating +the resource queues the ImageRepository for reconciliation if the +`` differs from the last value the controller acted on, as +reported in [`.status.lastHandledReconcileAt`](#last-handled-reconcile-at). + +Using `kubectl`: + +```sh +kubectl annotate --field-manager=flux-client-side-apply --overwrite imagerepository/ reconcile.fluxcd.io/requestedAt="$(date +%s)" +``` + +Using `flux`: + +```sh +flux reconcile image repository +``` + +### Waiting for `Ready` + +When a change is applied, it is possible to wait for the ImageRepository to +reach a [ready state](#ready-imagerepository) using `kubectl`: + +```sh +kubectl wait imagerepository/ --for=condition=ready --timeout=1m +``` + +### Suspending and resuming + +When you find yourself in a situation where you temporarily want to pause the +reconciliation of a ImageRepository, you can suspend it using the +[`.spec.suspend` field](#suspend). + +#### Suspend an ImageRepository + +In your YAML declaration: + +```yaml +--- +apiVersion: source.toolkit.fluxcd.io/v1beta3 +kind: ImageRepository +metadata: + name: +spec: + suspend: true +``` + +Using `kubectl`: + +```sh +kubectl patch imagerepository --field-manager=flux-client-side-apply -p '{\"spec\": {\"suspend\" : true }}' +``` + +Using `flux`: + +```sh +flux suspend image repository +``` + +**Note:** When an ImageRepository has scan results and is suspended, and this +result later disappears from the database due to e.g. the +image-reflector-controller Pod being evicted from a Node, this will not be +reflected in the ImageRepository's Status until it is resumed. + +#### Resume an ImageRepository + +In your YAML declaration, comment out (or remove) the `.spec.suspend` field: + +```yaml +--- +apiVersion: source.toolkit.fluxcd.io/v1beta3 +kind: ImageRepository +metadata: + name: +spec: + # suspend: true +``` + +**Note:** Setting the field value to `false` has the same effect as removing +it, but does not allow for "hot patching" using e.g. `kubectl` while practicing +GitOps; as the manually applied patch would be overwritten by the declared +state in Git. + +Using `kubectl`: + +```sh +kubectl patch imagerepository --field-manager=flux-client-side-apply -p '{\"spec\" : {\"suspend\" : false }}' +``` + +Using `flux`: + +```sh +flux resume image repository +``` + +### Debugging an ImageRepository + +There are several ways to gather information about an ImageRepository for +debugging purposes. + +#### Describe the ImageRepository + +Describing an ImageRepository using +`kubectl describe imagerepository ` +displays the latest recorded information for the resource in the `Status` and +`Events` sections: + +```console +... +Status: + Conditions: + Last Transition Time: 2022-09-19T05:47:40Z + Message: could not parse reference: ghcr.io/stefanprodan/podinfo:foo:bar + Observed Generation: 1 + Reason: ImageURLInvalid + Status: True + Type: Stalled + Last Transition Time: 2022-09-19T05:47:40Z + Message: could not parse reference: ghcr.io/stefanprodan/podinfo:foo:bar + Observed Generation: 1 + Reason: ImageURLInvalid + Status: False + Type: Ready + Observed Generation: 1 +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Warning ImageURLInvalid 5s image-reflector-controller could not parse reference: ghcr.io/stefanprodan/podinfo:foo:bar +``` + +#### Trace emitted Events + +To view events for specific ImageRepository(s), `kubectl events` can be used +in combination with `--for` to list the Events for specific objects. For +example, running + +```sh +kubectl events --for ImageRepository/ +``` + +lists + +```console +LAST SEEN TYPE REASON OBJECT MESSAGE +3m51s Normal Succeeded imagerepository/ successful scan, found 34 tags +114s Warning ImageURLInvalid imagerepository/ could not parse reference: ghcr.io/stefanprodan/podinfo:foo:bar +``` + +Besides being reported in Events, the reconciliation errors are also logged by +the controller. The Flux CLI offer commands for filtering the logs for a +specific ImageRepository, e.g. +`flux logs --level=error --kind=ImageRepository --name=`. + +## ImageRepository Status + +### Last Scan Result + +The ImageRepository reports the latest scanned tags from the image repository in +`.status.lastScanResult` for the resource. The tags are stored in an internal +database. `.status.lastScanResult.scanTime` shows the time of last scan. +`.status.lastScanResult.tagCount` shows the number of tags in the result. This +is calculated after applying any exclusion list rules. + +Example: +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta3 +kind: ImageRepository +metadata: + name: +status: + lastScanResult: + latestTags: + - latest + - 6.2.0 + - 6.1.8 + - 6.1.7 + - 6.1.6 + - 6.1.5 + - 6.1.4 + - 6.1.3 + - 6.1.2 + - 6.1.1 + scanTime: "2022-09-19T05:53:27Z" + tagCount: 34 +``` + +### Canonical Image Name + +The ImageRepository reports the canonical form of the image repository provided +in the ImageRepository's `.spec.image` in `.status.canonicalImageName`. +Canonical name is the name of the image repository with all the implied bits +made explicit; e.g., `docker.io/library/alpine` rather than `alpine`. + +### Observed Exclusion List + +The ImageRepository reports an observed exclusion list in the ImageRepository's +`.status.observedExclusionList`. The observed exclusion list is the latest +`.spec.exclusionList` which resulted in a [ready state](#ready-imagerepository), +or stalled due to error it can not recover from without human intervention. + +### Conditions + +An ImageRepository enters various states during its lifecycle, reflected as +[Kubernetes Conditions][typical-status-properties]. +It can be [reconciling](#reconciling-imagerepository) while scanning the image +repository, it can be [ready](#ready-imagerepository), or it can [fail during +reconciliation](#failed-imagerepository). + +The ImageRepository API is compatible with the [kstatus specification][kstatus-spec], +and reports `Reconciling` and `Stalled` conditions where applicable to provide +better (timeout) support to solutions polling the ImageRepository to become +`Ready`. + +#### Reconciling ImageRepository + +The image-reflector-controller marks an ImageRepository as _reconciling_ when +one of the following is true: + +- The generation of the ImageRepository is newer than the [Observed +Generation](#observed-generation). +- The ImageRepository is being scanned because it's scan time as per the + specified `spec.interval`, or the ImageRepository has never been scanned + before, or the reported tags in the last scanned results have disappeared + from the database. + +When the ImageRepository is "reconciling", the `Ready` Condition status becomes +`False`, and the controller adds a Condition with the following attributes to +the ImageRepository's `.status.conditions`: + +- `type: Reconciling` +- `status: "True"` +- `reason: NewGeneration` | `reason: Scanning` + +It has a ["negative polarity"][typical-status-properties], and is only present +on the ImageRepository while its status value is `"True"`. + +#### Ready ImageRepository + +The image-reflector-controller marks an ImageRepository as _ready_ when it has +the following characteristics: + +- The ImageRepository reports a [Last Scan Result](#last-scan-result). +- The reported tags exists in the controller's internal database. +- The controller was able to communicate with the remote image repository using + the current spec. + +When the ImageRepository is "ready", the controller sets a Condition with the +following attributes in the ImageRepository's `.status.conditions`: + +- `type: Ready` +- `status: "True"` +- `reason: Succeeded` + +This `Ready` Condition will retain a status value of `"True"` until the +ImageRepository is marked as [reconciling](#reconciling-imagerepository), or +e.g. a [transient error](#failed-imagerepository) occurs due to a temporary +network issue. + +#### Failed ImageRepository + +The image-reflector-controller may get stuck trying to scan an image repository +without completing. This can occur due to some of the following factors: + +- The remote image repository is temporarily unavailable. +- The image repository does not exist. +- The [Secret reference](#secret-reference) and [Certificate secret reference](#certificate-secret-reference) + contains a reference to a non-existing Secret. +- The credentials and certificate in the referenced Secret are invalid. +- The ImageRepository spec contains a generic misconfiguration. +- A database related failure when reading or writing the scanned tags. + +When this happens, the controller sets the `Ready` Condition status to `False` +with the following reasons: + +- `reason: ImageURLInvalid` | `reason: AuthenticationFailed` | `reason: Failure` | `reason: ReadOperationFailed` + +While the ImageRepository is in failing state, the controller will continue to +attempt to scan the image repository for the resource with an exponential +backoff, until it succeeds and the ImageRepository is marked as +[ready](#ready-imagerepository). + +Note that an ImageRepository can be [reconciling](#reconciling-imagerepository) +while failing at the same time, for example due to a newly introduced +configuration issue in the ImageRepository spec. + +### Observed Generation + +The image-reflector-controller reports an +[observed generation][typical-status-properties] in the ImageRepository's +`.status.observedGeneration`. The observed generation is the latest +`.metadata.generation` which resulted in either a +[ready state](#ready-imagerepository), or stalled due to error it can not +recover from without human intervention. + +### Last Handled Reconcile At + +The image-reflector-controller reports the last +`reconcile.fluxcd.io/requestedAt` annotation value it acted on in the +`.status.lastHandledReconcileAt` field. + +For practical information about this field, see [triggering a +reconcile](#triggering-a-reconcile). + +[image-auto-provider-secrets]: https://fluxcd.io/flux/guides/image-update/#imagerepository-cloud-providers-authentication +[pem-encoding]: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail +[sops-guide]: https://fluxcd.io/flux/guides/mozilla-sops/ +[cloud providers authentication guide]: https://fluxcd.io/flux/guides/image-update/#imagerepository-cloud-providers-authentication +[typical-status-properties]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties +[kstatus-spec]: https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index 44d2aa16..eb512145 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,5 +1,5 @@ /* -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/internal/controller/controllers_fuzzer_test.go b/internal/controller/controllers_fuzzer_test.go index 98c03df8..9e471762 100644 --- a/internal/controller/controllers_fuzzer_test.go +++ b/internal/controller/controllers_fuzzer_test.go @@ -49,7 +49,7 @@ import ( fuzz "github.com/AdaLogics/go-fuzz-headers" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" "github.com/fluxcd/image-reflector-controller/internal/database" "github.com/fluxcd/image-reflector-controller/internal/test" ) diff --git a/internal/controller/imagepolicy_controller.go b/internal/controller/imagepolicy_controller.go index 2f701e3a..19091e23 100644 --- a/internal/controller/imagepolicy_controller.go +++ b/internal/controller/imagepolicy_controller.go @@ -48,7 +48,7 @@ import ( "github.com/fluxcd/pkg/runtime/patch" pkgreconcile "github.com/fluxcd/pkg/runtime/reconcile" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" "github.com/fluxcd/image-reflector-controller/internal/policy" "github.com/fluxcd/image-reflector-controller/internal/registry" ) @@ -257,7 +257,7 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP } // Cleanup the last result. - obj.Status.LatestImage = "" + obj.Status.LatestRef = imagev1.ImageRef{} // Get ImageRepository from reference. repo, err := r.getImageRepository(ctx, obj) @@ -316,28 +316,29 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP } // Write the observations on status. - obj.Status.LatestImage = repo.Spec.Image + ":" + latest + obj.Status.LatestRef.Name, obj.Status.LatestRef.Tag = repo.Spec.Image, latest // If the old latest image and new latest image don't match, set the old // image as the observed previous image. // NOTE: The following allows the previous image to be set empty when // there's a failure and a subsequent recovery from it. This behavior helps // avoid creating an update event as there's no previous image to infer // from. Recovery from a failure shouldn't result in an update event. - if oldObj.Status.LatestImage != obj.Status.LatestImage { - obj.Status.ObservedPreviousImage = oldObj.Status.LatestImage + if oldObj.Status.LatestRef != obj.Status.LatestRef { + obj.Status.ObservedPreviousRef = oldObj.Status.LatestRef.DeepCopy() } - if err := r.updateDigest(ctx, repo, obj, latest); err != nil { + if err := r.updateDigest(ctx, repo, obj, oldObj, 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 != "" { - prevRef, err := name.NewTag(obj.Status.ObservedPreviousImage) + if obj.Status.ObservedPreviousRef != nil { + img := obj.Status.ObservedPreviousRef.Name + ":" + obj.Status.ObservedPreviousRef.Tag + prevRef, err := name.NewTag(img) if err != nil { - e := fmt.Errorf("failed to parse previous image '%s': %w", obj.Status.ObservedPreviousImage, err) + e := fmt.Errorf("failed to parse previous image '%s': %w", img, err) conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, e.Error()) result, retErr = ctrl.Result{}, e } @@ -353,22 +354,24 @@ 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 { +func (r *ImagePolicyReconciler) updateDigest(ctx context.Context, repo *imagev1.ImageRepository, obj, oldObj *imagev1.ImagePolicy, tag string) error { if obj.Spec.DigestReflectionPolicy == nil { - obj.Status.LatestDigest = "" + obj.Status.LatestRef.Digest = "" return nil } if *obj.Spec.DigestReflectionPolicy == imagev1.ReflectIfNotPresent && - obj.Status.LatestDigest != "" && - (obj.Status.ObservedPreviousImage == "" || obj.Status.ObservedPreviousImage == obj.Status.LatestImage) { + oldObj.Status.LatestRef.Digest != "" && + obj.Status.LatestRef.Name == oldObj.Status.LatestRef.Name && + obj.Status.LatestRef.Tag == oldObj.Status.LatestRef.Tag { + obj.Status.LatestRef.Digest = oldObj.Status.LatestRef.Digest return nil } var err error - obj.Status.LatestDigest, err = r.fetchDigest(ctx, repo, tag, obj) + obj.Status.LatestRef.Digest, err = r.fetchDigest(ctx, repo, tag, obj) if err != nil { - return fmt.Errorf("failed fetching digest of %s: %w", obj.Status.LatestImage, err) + return fmt.Errorf("failed fetching digest of %s: %w", obj.Status.LatestRef.String(), err) } return nil diff --git a/internal/controller/imagepolicy_controller_test.go b/internal/controller/imagepolicy_controller_test.go index 618d75c0..ac3a6b52 100644 --- a/internal/controller/imagepolicy_controller_test.go +++ b/internal/controller/imagepolicy_controller_test.go @@ -37,7 +37,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" "github.com/fluxcd/image-reflector-controller/internal/policy" "github.com/fluxcd/image-reflector-controller/internal/registry" "github.com/fluxcd/image-reflector-controller/internal/test" @@ -466,7 +466,7 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { c.Get(context.Background(), client.ObjectKeyFromObject(imagePol), imagePol), ).To(Succeed(), "failed getting image policy") - g.Expect(imagePol.Status.LatestDigest). + g.Expect(imagePol.Status.LatestRef.Digest). 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 @@ -517,7 +517,7 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { c.Get(context.Background(), client.ObjectKeyFromObject(imagePol), imagePol), ).To(Succeed(), "failed getting image policy") - g.Expect(imagePol.Status.LatestDigest). + g.Expect(imagePol.Status.LatestRef.Digest). To(Equal(tt.digest2ndPass()), "unexpected 2nd pass digest in status") }) } diff --git a/internal/controller/imagerepository_controller.go b/internal/controller/imagerepository_controller.go index f1156a34..507717ad 100644 --- a/internal/controller/imagerepository_controller.go +++ b/internal/controller/imagerepository_controller.go @@ -47,7 +47,7 @@ import ( "github.com/fluxcd/pkg/runtime/predicates" "github.com/fluxcd/pkg/runtime/reconcile" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" "github.com/fluxcd/image-reflector-controller/internal/registry" ) diff --git a/internal/controller/imagerepository_controller_test.go b/internal/controller/imagerepository_controller_test.go index b14efe15..bcb8607f 100644 --- a/internal/controller/imagerepository_controller_test.go +++ b/internal/controller/imagerepository_controller_test.go @@ -33,7 +33,7 @@ import ( "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" "github.com/fluxcd/image-reflector-controller/internal/registry" "github.com/fluxcd/image-reflector-controller/internal/test" ) diff --git a/internal/controller/policy_test.go b/internal/controller/policy_test.go index e442e5f2..0ce537ef 100644 --- a/internal/controller/policy_test.go +++ b/internal/controller/policy_test.go @@ -35,7 +35,7 @@ import ( conditionscheck "github.com/fluxcd/pkg/runtime/conditions/check" "github.com/fluxcd/pkg/runtime/patch" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" "github.com/fluxcd/image-reflector-controller/internal/database" "github.com/fluxcd/image-reflector-controller/internal/test" // +kubebuilder:scaffold:imports @@ -220,9 +220,9 @@ func TestImagePolicyReconciler_calculateImageFromRepoTags(t *testing.T) { g.Eventually(func() bool { err := testEnv.Get(ctx, polName, &pol) return err == nil && - pol.Status.LatestImage != "" + pol.Status.LatestRef != imagev1.ImageRef{} }, timeout, interval).Should(BeTrue()) - g.Expect(pol.Status.LatestImage).To(Equal(imgRepo + tt.wantImageTag)) + g.Expect(pol.Status.LatestRef.String()).To(Equal(imgRepo + tt.wantImageTag)) } else { g.Eventually(func() bool { err := testEnv.Get(ctx, polName, &pol) @@ -330,9 +330,9 @@ func TestImagePolicyReconciler_filterTags(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.LatestRef != imagev1.ImageRef{} }, timeout, interval).Should(BeTrue()) - g.Expect(pol.Status.LatestImage).To(Equal(imgRepo + tt.wantImageTag)) + g.Expect(pol.Status.LatestRef.String()).To(Equal(imgRepo + tt.wantImageTag)) } else { g.Eventually(func() bool { err := testEnv.Get(ctx, polName, &pol) @@ -506,9 +506,9 @@ func TestImagePolicyReconciler_accessImageRepo(t *testing.T) { if tt.wantAccessible { g.Eventually(func() bool { err := testEnv.Get(ctx, polName, &pol) - return err == nil && pol.Status.LatestImage != "" + return err == nil && pol.Status.LatestRef.Name != "" }, timeout, interval).Should(BeTrue()) - g.Expect(pol.Status.LatestImage).To(Equal(imgRepo + ":1.0.1")) + g.Expect(pol.Status.LatestRef.String()).To(Equal(imgRepo + ":1.0.1")) } else { g.Eventually(func() bool { _ = testEnv.Get(ctx, polName, &pol) diff --git a/internal/controller/scan_test.go b/internal/controller/scan_test.go index f7c999a3..864182d8 100644 --- a/internal/controller/scan_test.go +++ b/internal/controller/scan_test.go @@ -37,7 +37,7 @@ import ( fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" "github.com/fluxcd/image-reflector-controller/internal/database" "github.com/fluxcd/image-reflector-controller/internal/test" // +kubebuilder:scaffold:imports diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index cf4dfd64..c1b92a9d 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -35,7 +35,7 @@ import ( "github.com/fluxcd/pkg/runtime/controller" "github.com/fluxcd/pkg/runtime/testenv" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" "github.com/fluxcd/image-reflector-controller/internal/database" "github.com/fluxcd/image-reflector-controller/internal/registry" // +kubebuilder:scaffold:imports diff --git a/internal/policy/factory.go b/internal/policy/factory.go index 3d70140f..4140a545 100644 --- a/internal/policy/factory.go +++ b/internal/policy/factory.go @@ -20,7 +20,7 @@ import ( "fmt" "strings" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" ) // PolicerFromSpec constructs a new policy object based on diff --git a/internal/policy/factory_test.go b/internal/policy/factory_test.go index 37d0428f..f15f63f6 100644 --- a/internal/policy/factory_test.go +++ b/internal/policy/factory_test.go @@ -19,7 +19,7 @@ package policy import ( "testing" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" ) func TestFactory_PolicerFromSpec(t *testing.T) { diff --git a/internal/registry/helper.go b/internal/registry/helper.go index af3b736e..92954774 100644 --- a/internal/registry/helper.go +++ b/internal/registry/helper.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" "github.com/fluxcd/pkg/oci/auth/login" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" diff --git a/internal/registry/helper_test.go b/internal/registry/helper_test.go index b401a77e..1fb42294 100644 --- a/internal/registry/helper_test.go +++ b/internal/registry/helper_test.go @@ -28,7 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" "github.com/fluxcd/image-reflector-controller/internal/registry" "github.com/fluxcd/image-reflector-controller/internal/secret" "github.com/fluxcd/image-reflector-controller/internal/test" diff --git a/internal/registry/options.go b/internal/registry/options.go index 06cdf5a1..794ce220 100644 --- a/internal/registry/options.go +++ b/internal/registry/options.go @@ -14,7 +14,7 @@ import ( "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" "github.com/fluxcd/image-reflector-controller/internal/secret" ) diff --git a/main.go b/main.go index 10b834a9..56e92eab 100644 --- a/main.go +++ b/main.go @@ -48,7 +48,7 @@ import ( // +kubebuilder:scaffold:imports - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" "github.com/fluxcd/image-reflector-controller/internal/controller" "github.com/fluxcd/image-reflector-controller/internal/database" "github.com/fluxcd/image-reflector-controller/internal/features" diff --git a/tests/integration/imagerepo_test.go b/tests/integration/imagerepo_test.go index 5ba0e574..01d9d97e 100644 --- a/tests/integration/imagerepo_test.go +++ b/tests/integration/imagerepo_test.go @@ -25,7 +25,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" ) func TestImageRepositoryScan(t *testing.T) { diff --git a/tests/integration/suite_test.go b/tests/integration/suite_test.go index c9b37ac0..910bd6cf 100644 --- a/tests/integration/suite_test.go +++ b/tests/integration/suite_test.go @@ -29,7 +29,7 @@ import ( tfjson "github.com/hashicorp/terraform-json" "k8s.io/apimachinery/pkg/runtime" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" "github.com/fluxcd/test-infra/tftestenv" ) From 111fe058c11b4eea512e9882bc88654ee643a302 Mon Sep 17 00:00:00 2001 From: Max Jonas Werner Date: Wed, 30 Aug 2023 15:45:15 +0200 Subject: [PATCH 03/17] Add "Never" as a valid value for .spec.digestReflectionPolicy This way we circumvent issues with server-side apply so that users can explicitly change this field instead of having to remove it. The latter case might lead to the API server not removing it if another field manager is registered for that field, causing an unintended drift. This commit also aligns the v1beta3 API with the latest changes done in v1beta2. Signed-off-by: Max Jonas Werner --- api/v1beta3/imagepolicy_types.go | 6 ++- api/v1beta3/imagerepository_types.go | 22 ++++++---- ...image.toolkit.fluxcd.io_imagepolicies.yaml | 1 + ...e.toolkit.fluxcd.io_imagerepositories.yaml | 10 +++-- docs/spec/v1beta2/imagepolicies.md | 41 +------------------ docs/spec/v1beta3/imagepolicies.md | 5 ++- internal/controller/imagepolicy_controller.go | 4 +- .../controller/imagepolicy_controller_test.go | 22 +++++++--- 8 files changed, 50 insertions(+), 61 deletions(-) diff --git a/api/v1beta3/imagepolicy_types.go b/api/v1beta3/imagepolicy_types.go index 4385dd2e..ae47e985 100644 --- a/api/v1beta3/imagepolicy_types.go +++ b/api/v1beta3/imagepolicy_types.go @@ -22,6 +22,8 @@ import ( ) const ImagePolicyKind = "ImagePolicy" + +// Deprecated: Use ImageFinalizer. const ImagePolicyFinalizer = "finalizers.fluxcd.io" // ImagePolicySpec defines the parameters for calculating the @@ -46,7 +48,7 @@ type ImagePolicySpec struct { } // ReflectionPolicy describes a policy for if/when to reflect a value from the registry in a certain resource field. -// +kubebuilder:validation:Enum=Always;IfNotPresent +// +kubebuilder:validation:Enum=Always;IfNotPresent;Never type ReflectionPolicy string const ( @@ -56,6 +58,8 @@ const ( // 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" + // ReflectNever means that no reflection will happen at all. + ReflectNever ReflectionPolicy = "Never" ) // ImagePolicyChoice is a union of all the types of policy that can be diff --git a/api/v1beta3/imagerepository_types.go b/api/v1beta3/imagerepository_types.go index 1566dc65..7bac4e76 100644 --- a/api/v1beta3/imagerepository_types.go +++ b/api/v1beta3/imagerepository_types.go @@ -26,6 +26,8 @@ import ( ) const ImageRepositoryKind = "ImageRepository" + +// Deprecated: Use ImageFinalizer. const ImageRepositoryFinalizer = "finalizers.fluxcd.io" // ImageRepositorySpec defines the parameters for scanning an image @@ -61,17 +63,21 @@ type ImageRepositorySpec struct { // +optional ServiceAccountName string `json:"serviceAccountName,omitempty"` - // CertSecretRef can be given the name of a secret containing + // 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`) + // - a PEM-encoded client certificate (`tls.crt`) and private + // key (`tls.key`); + // - a PEM-encoded CA certificate (`ca.crt`) + // + // 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. The Secret must + // be of type `Opaque` or `kubernetes.io/tls`. // - // 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. + // Note: Support for the `caFile`, `certFile` and `keyFile` keys has + // been deprecated. // +optional CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"` diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml index 110416dc..53b876e5 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml @@ -431,6 +431,7 @@ spec: enum: - Always - IfNotPresent + - Never type: string filterTags: description: FilterTags enables filtering for only a subset of tags diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml index 82528b04..eec6c74f 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml @@ -529,13 +529,15 @@ spec: - 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`) + description: "CertSecretRef can be given the name of a Secret containing + either or both of \n - a PEM-encoded client certificate (`tls.crt`) + and private key (`tls.key`); - a PEM-encoded CA certificate (`ca.crt`) \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." + server certificate. The Secret must be of type `Opaque` or `kubernetes.io/tls`. + \n Note: Support for the `caFile`, `certFile` and `keyFile` keys + has been deprecated." properties: name: description: Name of the referent. diff --git a/docs/spec/v1beta2/imagepolicies.md b/docs/spec/v1beta2/imagepolicies.md index 989dbf35..8b73621f 100644 --- a/docs/spec/v1beta2/imagepolicies.md +++ b/docs/spec/v1beta2/imagepolicies.md @@ -38,8 +38,7 @@ 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` field. -- The selected tag's digest is reported in the `.status.latestDigest` field. + selected tag, and reported in the `.status.latestImage`. This example can be run by saving the manifest into `imagepolicy.yaml`. @@ -68,7 +67,6 @@ Status: Reason: Succeeded Status: True Type: Ready - Latest Digest: sha256:2d9a00b3981628a533ff43352193b1838b0a4bf6b0033444286f563205e51a2c Latest Image: ghcr.io/stefanprodan/podinfo:5.1.4 Observed Generation: 1 Events: @@ -252,19 +250,6 @@ 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 @@ -348,7 +333,7 @@ specific ImagePolicy, e.g. ### Latest Image -The ImagePolicy reports the latest selected image from the ImageRepository tags in +The ImagePolicy reports the latest select image from the ImageRepository tags in `.status.latestImage` for the resource. Example: @@ -361,28 +346,6 @@ 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/docs/spec/v1beta3/imagepolicies.md b/docs/spec/v1beta3/imagepolicies.md index 51c2b333..c9b0b3e8 100644 --- a/docs/spec/v1beta3/imagepolicies.md +++ b/docs/spec/v1beta3/imagepolicies.md @@ -1,5 +1,7 @@ # Image Policies + + The `ImagePolicies` API defines rules for selecting a "latest" image from `ImageRepositories`. @@ -19,6 +21,7 @@ metadata: spec: imageRepositoryRef: name: podinfo + digestReflectionPolicy: IfNotPresent policy: semver: range: 5.1.x @@ -255,7 +258,7 @@ used in the policy rule to determine the latest tag. `.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. +- `Never`: If the field is set to `Never` (or not present) 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 diff --git a/internal/controller/imagepolicy_controller.go b/internal/controller/imagepolicy_controller.go index 19091e23..39612b59 100644 --- a/internal/controller/imagepolicy_controller.go +++ b/internal/controller/imagepolicy_controller.go @@ -355,12 +355,12 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP } func (r *ImagePolicyReconciler) updateDigest(ctx context.Context, repo *imagev1.ImageRepository, obj, oldObj *imagev1.ImagePolicy, tag string) error { - if obj.Spec.DigestReflectionPolicy == nil { + if obj.Spec.DigestReflectionPolicy == nil || strings.EqualFold(string(*obj.Spec.DigestReflectionPolicy), string(imagev1.ReflectNever)) { obj.Status.LatestRef.Digest = "" return nil } - if *obj.Spec.DigestReflectionPolicy == imagev1.ReflectIfNotPresent && + if strings.EqualFold(string(*obj.Spec.DigestReflectionPolicy), string(imagev1.ReflectIfNotPresent)) && oldObj.Status.LatestRef.Digest != "" && obj.Status.LatestRef.Name == oldObj.Status.LatestRef.Name && obj.Status.LatestRef.Tag == oldObj.Status.LatestRef.Tag { diff --git a/internal/controller/imagepolicy_controller_test.go b/internal/controller/imagepolicy_controller_test.go index ac3a6b52..0bbc257c 100644 --- a/internal/controller/imagepolicy_controller_test.go +++ b/internal/controller/imagepolicy_controller_test.go @@ -270,9 +270,9 @@ func TestImagePolicyReconciler_getImageRepository(t *testing.T) { } func TestImagePolicyReconciler_digestReflection(t *testing.T) { - polAlways := imagev1.ReflectAlways - polIfNotPresent := imagev1.ReflectIfNotPresent - + polAlways := imagev1.ReflectionPolicy("AlWaYs") + polIfNotPresent := imagev1.ReflectionPolicy("IfNoTpReSeNt") + polNever := imagev1.ReflectionPolicy("NeVeR") registryServer := test.NewRegistryServer() defer registryServer.Close() @@ -293,7 +293,7 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { digest2ndPass func() string }{ { - name: "nil/missing policy leaves digest empty", + name: "missing policy leaves digest empty", refPolicy1stPass: nil, digest1stPass: func() string { return "" @@ -302,6 +302,16 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { return "" }, }, + { + name: "'Never' policy leaves digest empty", + refPolicy1stPass: &polNever, + digest1stPass: func() string { + return "" + }, + digest2ndPass: func() string { + return "" + }, + }, { name: "'Always' policy always updates digest", refPolicy1stPass: &polAlways, @@ -339,7 +349,7 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { { name: "unsetting 'Always' policy removes digest", refPolicy1stPass: &polAlways, - refPolicy2ndPass: nil, + refPolicy2ndPass: &polNever, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, @@ -350,7 +360,7 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { { name: "unsetting 'IfNotPresent' policy removes digest", refPolicy1stPass: &polIfNotPresent, - refPolicy2ndPass: nil, + refPolicy2ndPass: &polNever, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, From 5f4903b1c5a910bbe35edc1cf7709e62e5bb30cd Mon Sep 17 00:00:00 2001 From: Max Jonas Werner Date: Tue, 5 Sep 2023 13:42:10 +0200 Subject: [PATCH 04/17] Revert introduction of v1beta3 We agreed to make the changes in the existing v1beta2 API version. Signed-off-by: Max Jonas Werner --- Makefile | 2 +- PROJECT | 6 - api/v1beta2/imagepolicy_types.go | 50 +- api/v1beta2/imagerepository_types.go | 1 + api/v1beta2/zz_generated.deepcopy.go | 26 + api/v1beta3/condition_types.go | 35 - api/v1beta3/doc.go | 24 - api/v1beta3/groupversion_info.go | 36 - api/v1beta3/imagepolicy_types.go | 193 ----- api/v1beta3/imagerepository_types.go | 215 ----- api/v1beta3/zz_generated.deepcopy.go | 402 --------- ...image.toolkit.fluxcd.io_imagepolicies.yaml | 205 +---- ...e.toolkit.fluxcd.io_imagerepositories.yaml | 242 ------ config/samples/image_v1beta3_imagepolicy.yaml | 11 - .../image_v1beta3_imagerepository.yaml | 12 - .../{v1beta3 => v1beta2}/image-reflector.md | 122 ++- docs/spec/v1beta3/imagepolicies.md | 498 ----------- docs/spec/v1beta3/imagerepositories.md | 811 ------------------ .../controller/controllers_fuzzer_test.go | 2 +- internal/controller/imagepolicy_controller.go | 5 +- .../controller/imagepolicy_controller_test.go | 2 +- .../controller/imagerepository_controller.go | 2 +- .../imagerepository_controller_test.go | 2 +- internal/controller/policy_test.go | 2 +- 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 +- 34 files changed, 181 insertions(+), 2745 deletions(-) delete mode 100644 api/v1beta3/condition_types.go delete mode 100644 api/v1beta3/doc.go delete mode 100644 api/v1beta3/groupversion_info.go delete mode 100644 api/v1beta3/imagepolicy_types.go delete mode 100644 api/v1beta3/imagerepository_types.go delete mode 100644 api/v1beta3/zz_generated.deepcopy.go delete mode 100644 config/samples/image_v1beta3_imagepolicy.yaml delete mode 100644 config/samples/image_v1beta3_imagerepository.yaml rename docs/api/{v1beta3 => v1beta2}/image-reflector.md (84%) delete mode 100644 docs/spec/v1beta3/imagepolicies.md delete mode 100644 docs/spec/v1beta3/imagerepositories.md diff --git a/Makefile b/Makefile index 48a7c544..5db80131 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/v1beta3 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/v1beta3/image-reflector.md + $(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 # Run go mod tidy tidy: diff --git a/PROJECT b/PROJECT index 22ff89dc..87f3dcb3 100644 --- a/PROJECT +++ b/PROJECT @@ -13,10 +13,4 @@ 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/v1beta2/imagepolicy_types.go b/api/v1beta2/imagepolicy_types.go index c4b9d6dd..be5fb88c 100644 --- a/api/v1beta2/imagepolicy_types.go +++ b/api/v1beta2/imagepolicy_types.go @@ -1,5 +1,5 @@ /* -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. @@ -42,8 +42,26 @@ 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;Never +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" + // ReflectNever means that no reflection will happen at all. + ReflectNever ReflectionPolicy = "Never" +) + // ImagePolicyChoice is a union of all the types of policy that can be // supplied. type ImagePolicyChoice struct { @@ -101,16 +119,45 @@ type TagFilter struct { 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 { // 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. LatestImage string `json:"latestImage,omitempty"` // 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. // +optional ObservedPreviousImage string `json:"observedPreviousImage,omitempty"` + // 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 @@ -127,6 +174,7 @@ 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 0766509e..eaee2c14 100644 --- a/api/v1beta2/imagerepository_types.go +++ b/api/v1beta2/imagerepository_types.go @@ -185,6 +185,7 @@ 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 ba5d2dd6..34ec2ffb 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -142,6 +142,11 @@ 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. @@ -157,6 +162,12 @@ 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.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)) @@ -176,6 +187,21 @@ func (in *ImagePolicyStatus) DeepCopy() *ImagePolicyStatus { 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 diff --git a/api/v1beta3/condition_types.go b/api/v1beta3/condition_types.go deleted file mode 100644 index 373abb85..00000000 --- a/api/v1beta3/condition_types.go +++ /dev/null @@ -1,35 +0,0 @@ -/* -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 deleted file mode 100644 index 9329202e..00000000 --- a/api/v1beta3/doc.go +++ /dev/null @@ -1,24 +0,0 @@ -/* -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 deleted file mode 100644 index cad8be83..00000000 --- a/api/v1beta3/groupversion_info.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -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 deleted file mode 100644 index ae47e985..00000000 --- a/api/v1beta3/imagepolicy_types.go +++ /dev/null @@ -1,193 +0,0 @@ -/* -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" - -// Deprecated: Use ImageFinalizer. -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;Never -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" - // ReflectNever means that no reflection will happen at all. - ReflectNever ReflectionPolicy = "Never" -) - -// 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 deleted file mode 100644 index 7bac4e76..00000000 --- a/api/v1beta3/imagerepository_types.go +++ /dev/null @@ -1,215 +0,0 @@ -/* -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" - -// Deprecated: Use ImageFinalizer. -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 (`tls.crt`) and private - // key (`tls.key`); - // - a PEM-encoded CA certificate (`ca.crt`) - // - // 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. The Secret must - // be of type `Opaque` or `kubernetes.io/tls`. - // - // Note: Support for the `caFile`, `certFile` and `keyFile` keys has - // been deprecated. - // +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 deleted file mode 100644 index f6acc381..00000000 --- a/api/v1beta3/zz_generated.deepcopy.go +++ /dev/null @@ -1,402 +0,0 @@ -//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 53b876e5..e670533a 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml @@ -210,201 +210,6 @@ 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 @@ -585,6 +390,11 @@ spec: - 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. Deprecated: Replaced by the composite "latestRef" field.' + type: string latestRef: description: LatestRef gives the first in the list of images scanned by the image repository, when filtered and ordered according to @@ -603,6 +413,11 @@ spec: 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. Deprecated: + Replaced by the composite "observedPreviousRef" field.' + type: string observedPreviousRef: description: ObservedPreviousRef is the observed previous LatestRef. It is used to keep track of the previous and current images. diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml index eec6c74f..40075d79 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml @@ -469,248 +469,6 @@ 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 (`tls.crt`) - and private key (`tls.key`); - a PEM-encoded CA certificate (`ca.crt`) - \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. The Secret must be of type `Opaque` or `kubernetes.io/tls`. - \n Note: Support for the `caFile`, `certFile` and `keyFile` keys - has been deprecated." - 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 deleted file mode 100644 index 99d3e340..00000000 --- a/config/samples/image_v1beta3_imagepolicy.yaml +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 1a0c4ade..00000000 --- a/config/samples/image_v1beta3_imagerepository.yaml +++ /dev/null @@ -1,12 +0,0 @@ -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/v1beta3/image-reflector.md b/docs/api/v1beta2/image-reflector.md similarity index 84% rename from docs/api/v1beta3/image-reflector.md rename to docs/api/v1beta2/image-reflector.md index 89e246de..bad64fb3 100644 --- a/docs/api/v1beta3/image-reflector.md +++ b/docs/api/v1beta2/image-reflector.md @@ -1,22 +1,22 @@ -

    Image reflector API reference v1beta3

    +

    Image reflector API reference v1beta2

    Packages:

    -

    image.toolkit.fluxcd.io/v1beta3

    -

    Package v1beta3 contains API types for the image API group, version -v1beta3. These types are concerned with reflecting metadata from +

    image.toolkit.fluxcd.io/v1beta2

    +

    Package v1beta2 contains API types for the image API group, version +v1beta2. 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:
      -

      AlphabeticalPolicy +

      AlphabeticalPolicy

      (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

      @@ -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)

      @@ -1018,11 +1046,11 @@ Kubernetes meta/v1.Time
      -

      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

      diff --git a/docs/spec/v1beta3/imagepolicies.md b/docs/spec/v1beta3/imagepolicies.md deleted file mode 100644 index c9b0b3e8..00000000 --- a/docs/spec/v1beta3/imagepolicies.md +++ /dev/null @@ -1,498 +0,0 @@ -# Image Policies - - - -The `ImagePolicies` API defines rules for selecting a "latest" image from -`ImageRepositories`. - -## Example - -The following is an example of an ImagePolicy. It queries the referred -ImageRepository for the image name of the repository, reads all the tags in -the repository and selects the latest tag based on the defined policy rules. - -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImagePolicy -metadata: - name: podinfo - namespace: default -spec: - imageRepositoryRef: - name: podinfo - digestReflectionPolicy: IfNotPresent - policy: - semver: - range: 5.1.x -``` - -In the above example: - -- An ImagePolicy named `podinfo` is created, indicated by the `.metadata.name` - field. -- The image-reflector-controller applies the latest tag selection policy every - time there's an update in the referred ImageRepository, indicated by the - `.spec.imageRepositoryRef.name` field. -- It fetches the canonical image name of the referred ImageRepository and reads - the scanned tags from the internal database for the image name. The read tags - 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` 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`. - -1. Apply the resource on the cluster: - -```sh -kubectl apply -f imagepolicy.yaml -``` - -2. Run `kubectl get imagepolicy` to see the ImagePolicy: - -```console -NAME LATESTIMAGE -podinfo ghcr.io/stefanprodan/podinfo:5.1.4 -``` - -3. Run `kubectl describe imagepolicy podinfo` to see the [Latest Image](#latest-image) -and [Conditions](#conditions) in the ImagePolicy's Status: - -```console -Status: - Conditions: - Last Transition Time: 2022-09-20T07:09:56Z - Message: Latest image tag for 'ghcr.io/stefanprodan/podinfo' resolved to 5.1.4 - Observed Generation: 1 - Reason: Succeeded - Status: True - Type: Ready - Latest Digest: sha256:2d9a00b3981628a533ff43352193b1838b0a4bf6b0033444286f563205e51a2c - Latest Image: ghcr.io/stefanprodan/podinfo:5.1.4 - Observed Generation: 1 -Events: - Type Reason Age From Message - ---- ------ ---- ---- ------- - Normal Succeeded 7s (x3 over 8s) image-reflector-controller Latest image tag for 'ghcr.io/stefanprodan/podinfo' resolved to 5.1.4 -``` - -## Writing an ImagePolicy spec - -As with all other Kubernetes config, an ImagePolicy needs `apiVersion`, -`kind`, and `metadata` fields. The name of an ImagePolicy object must be a -valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names). - -An ImagePolicy also needs a -[`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status). - -### Image Repository Reference - -`.spec.imageRepositoryRef` is a required field that specifies the -ImageRepository for which the latest image has to be selected. The value must be -a namespaced object reference. For ImageRepository in the same namespace as the -ImagePolicy, no namespace needs to be provided. For ImageRepository in a -different namespace than the namespace of the ImagePolicy, namespace name has to -be provided. For example: - -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImagePolicy -metadata: - name: podinfo - namespace: default -spec: - imageRepositoryRef: - name: podinfo - namespace: flux-system -... -``` - -The ImageRepository access is determied by its ACL for cross-namespace -reference. For more details on how to allow cross-namespace references see the -[ImageRepository docs](imagerepositories.md#access-from). - -### Policy - -`.spec.policy` is a required field that specifies how to choose a latest image -given the image metadata. There are three image policy choices: -- SemVer -- Alphabetical -- Numerical - -#### SemVer - -SemVer policy interprets all the tags as semver versions and chooses the highest -version available that fits the given -[semver constraints](https://github.com/Masterminds/semver#checking-version-constraints). -The constraint is set in the `.spec.policy.semver.range` field. - -Example of a SemVer image policy choice: - -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImagePolicy -metadata: - name: podinfo -spec: - imageRepositoryRef: - name: podinfo - policy: - semver: - range: '>=1.0.0' -``` - -This will select the latest stable version tag. - -#### Alphabetical - -Alphabetical policy chooses the _last_ tag when all the tags are sorted -alphabetically (in either ascending or descending order). The sort order is set -in the `.spec.policy.alphabetical.order` field. The value could be `asc` for -ascending order or `desc` for descending order. The default value is `asc`. - -Example of an Alphabetical policy choice: - -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImagePolicy -metadata: - name: podinfo -spec: - imageRepositoryRef: - name: podinfo - policy: - alphabetical: - order: asc -``` - -This will select the last tag when all the tags are sorted alphabetically in -ascending order. - -#### Numerical - -Numerical policy chooses the _last_ tag when all the tags are sorted numerically -(in either ascending or descending order). The sort order is set in the -`.spec.policy.numerical.order` field. The value could be `asc` for ascending -order or `desc` for descending order. The default value is `asc`. - -Example of a Numerical policy choice: - -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImagePolicy -metadata: - name: podinfo -spec: - imageRepositoryRef: - name: podinfo - policy: - numerical: - order: asc -``` - -This will select the last tag when all the tags are sorted numerically in -ascending order. - -### Filter Tags - -`.spec.filterTags` is an optional field to specify a filter on the image tags -before they are considered by the policy rule. - -The filter pattern is a regular expression, set in the -`.spec.filterTags.pattern` field. Only tags that match the pattern are -considered by the policy rule. - -The `.spec.filterTags.extract` is an optional field used to extract a value from -the matching tags which is supplied to the policy rule instead of the original -tags. If unspecified, the tags that match the pattern will be used as they are. - -Example of selecting the latest release candidate (semver): - -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImagePolicy -metadata: - name: podinfo -spec: - imageRepositoryRef: - name: podinfo - filterTags: - pattern: '.*-rc.*' - policy: - semver: - range: '^1.x-0' -``` - -Example of selecting the latest release tagged as `RELEASE.` -(alphabetical): - -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImagePolicy -metadata: - name: minio -spec: - imageRepositoryRef: - name: minio - filterTags: - pattern: '^RELEASE\.(?P.*)Z$' - extract: '$timestamp' - policy: - alphabetical: - order: asc -``` - -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: - -- `Never`: If the field is set to `Never` (or not present) 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 - -ImagePolicy is reconciled automatically when the associated ImageRepository is -updated. Whenever ImageRepository gets updated, ImagePolicy will be triggered -and have the policy result based on the latest values of ImageRepository. To -manually tell the image-reflector-controller to reconcile an ImagePolicy, the -associated ImageRepository can be annotated with -`reconcile.fluxcd.io/requestedAt: `. -See [triggering a reconcile](imagerepositories.md#triggering-a-reconcile) for -more details about reconciling ImageRepository. - -### Waiting for `Ready` - -When a change is applied, it is possible to wait for the ImagePolicy to reach a -[ready state](#ready-imagepolicy) using `kubectl`: - -```sh -kubectl wait imagepolicy/ --for=condition=ready --timeout=1m -``` - -### Debugging an ImagePolicy - -There are several ways to gather information about an ImagePolicy for debugging -purposes. - -#### Describe the ImagePolicy - -Describing an ImagePolicy using `kubectl describe imagepolicy ` -displays the latest recorded information for the resource in the `Status` and -`Events` sections: - -```console -... -Status: - Conditions: - Last Transition Time: 2022-10-06T12:07:35Z - Message: accessing ImageRepository - Observed Generation: 1 - Reason: AccessingRepository - Status: True - Type: Reconciling - Last Transition Time: 2022-10-06T12:07:35Z - Message: failed to get the referred ImageRepository: referenced ImageRepository does not exist: ImageRepository.image.toolkit.fluxcd.io "podinfo" not found - Observed Generation: 1 - Reason: DependencyNotReady - Status: False - Type: Ready - Observed Generation: 1 -Events: - Type Reason Age From Message - ---- ------ ---- ---- ------- - Warning DependencyNotReady 2s (x4 over 5s) image-reflector-controller failed to get the referred ImageRepository: referenced ImageRepository does not exist: ImageRepository.image.toolkit.fluxcd.io "podinfo" not found -``` - -#### Trace emitted Events - -To view events for specific ImagePolicy(s), `kubectl events` can be used in -combination with `--for` to list the Events for specific objects. For example, -running - -```sh -kubectl events --for ImagePolicy/ -``` - -lists - -```console -LAST SEEN TYPE REASON OBJECT MESSAGE -4m44s Normal Succeeded imagepolicy/ Latest image tag for 'ghcr.io/stefanprodan/podinfo' resolved to 5.1.4 -95s Warning DependencyNotReady imagepolicy/ failed to get the referred ImageRepository: referenced ImageRepository does not exist: ImageRepository.image.toolkit.fluxcd.io "podinfo" not found -``` - -Besides being reported in Events, the reconciliation errors are also logged by -the controller. The Flux CLI offer commands for filtering the logs for a -specific ImagePolicy, e.g. -`flux logs --level=error --kind=ImagePolicy --name=`. - -## ImagePolicy Status - -### Latest Image - -The ImagePolicy reports the latest selected image from the ImageRepository tags in -`.status.latestImage` for the resource. - -Example: - -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImagePolicy -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/v1beta3 -kind: ImagePolicy -metadata: - name: -status: - latestDigest: sha256:2d9a00b3981628a533ff43352193b1838b0a4bf6b0033444286f563205e51a2c -[...] -``` - -### Observed Previous Image - -The ImagePolicy reports the previously observed latest image in -`.status.observedPreviousImage` for the resource. This is used by the -ImagePolicy to determine an upgrade path of an ImagePolicy update. This field -is reset when the ImagePolicy fails due to some reason to be able to distinguish -between a failure recovery and a genuine latest image upgrade. - -Example: - -```yaml -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImagePolicy -metadata: - name: -status: - latestImage: ghcr.io/stefanprodan/podinfo:6.2.1 - observedPreviousImage: ghcr.io/stefanprodan/podinfo:5.1.4 -``` - -### Conditions - -An ImagePolicy enters various states during its lifecycle, reflected as -[Kubernetes Conditions][typical-status-properties]. -It can be [reconciling](#reconciling-imagepolicy) while reading the tags from -ImageRepository scan results, it can be [ready](#ready-imagepolicy), or it can -[fail during reconciliation](#failed-imagepolicy). - -The ImagePolicy API is compatible with the [kstatus specification][kstatus-spec], -and reports `Reconciling` and `Stalled` conditions where applicable to provide -better (timeout) support to solutions polling the ImagePolicy to become `Ready`. - -#### Reconciling ImagePolicy - -The image-reflector-controller marks an ImagePolicy as _reconciling_ when one of -the following is true: - -- The generation of the ImagePolicy is newer than the [Observed Generation](#observed-generation). -- The ImagePolicy is accessing the provided ImageRepository reference. -- The ImagePolicy is being applied to the tags read from an ImageRepository. - -When the ImagePolicy is "reconciling", the `Ready` Condition status becomes -`False`, and the controller adds a Condition with the following attributes to -the ImagePolicy's `.status.conditions`: - -- `type: Reconciling` -- `status: "True"` -- `reason: NewGeneration` | `reason:AccessingRepository` | `reason: ApplyingPolicy` - -It has a ["negative polarity"][typical-status-properties], and is only present -on the ImagePolicy while its status value is `"True"`. - -#### Ready ImagePolicy - -The image-reflector-controller marks an ImagePolicy as _ready_ when it has the -following characteristics: - -- The ImagePolicy reports a [Latest Image](#latest-image) -- The referenced ImageRepository is accessible and the internal tags database - contains the tags that ImagePolicy needs to apply the policy on. - -When the ImagePolicy is "ready", the controller sets a Condition with the -following attributes in the ImagePolicy's `.status.conditions`. - -- `type: Ready` -- `status: "True"` -- `reason: Succeeded` - -This `Ready` Condition will retain a status value of `"True"` until the -ImagePolicy is marked as [reconciling](#reconciling-imagepolicy), or e.g. a -[transient error](#failed-imagepolicy) occurs due to a temporary network issue. - -#### Failed ImagePolicy - -The image-reflector-controller may get stuck trying to apply a policy without -completing. This can occur due to some of the following factors: - -- The referenced ImageRepository is temporarily unavailable. -- The referenced ImageRepository does not exist. -- The referenced ImageRepository is not accessible in a different namespace. -- The ImagePolicy spec contains a generic misconfiguration. -- The ImagePolicy could not select the latest tag based on the given rules and - the available tags. -- A database related failure when reading or writing the scanned tags. - -When this happens, the controller sets the `Ready` condition status to `False` -wit the following reason: - -- `reason: Failure` | `reason: AccessDenied` | `reason: DependencyNotReady` - -While the ImagePolicy is in failing state, the controller will continue to -attempt to get the referenced ImageRepository for the resource and apply the -policy rules with an exponential backoff, until it succeeds and the ImagePolicy -is marked as [ready](#ready-imagepolicy). - -Note that an ImagePolicy can be [reconcilcing](#reconciling-imagepolicy) while -failing at the same time, for example due to a newly introduced configuration -issue in the ImagePolicy spec. - -### Observed Generation - -The image-reflector-controller reports an -[observed generation][typical-status-properties] in the ImagePolicy's -`.status.observedGeneration`. The observed generation is the latest -`.metadata.generation` which resulted in either a -[ready state](#ready-imagepolicy), or stalled due to error it can not -recover from without human intervention. - -[typical-status-properties]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties -[kstatus-spec]: https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus diff --git a/docs/spec/v1beta3/imagerepositories.md b/docs/spec/v1beta3/imagerepositories.md deleted file mode 100644 index 6666d107..00000000 --- a/docs/spec/v1beta3/imagerepositories.md +++ /dev/null @@ -1,811 +0,0 @@ -# Image Repositories - -The `ImageRepository` API defines a repository to scan and store a specific set -of tags in a database. - -## Example - -The following is an example of an ImageRepository. It scans the specified image -repository and stores the scanned tags in an internal database. - -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImageRepository -metadata: - name: podinfo - namespace: default -spec: - image: stefanprodan/podinfo - interval: 1h - provider: generic -``` - -In the above example: - -- An ImageRepository named `podinfo` is created, indicated by the - `.metadata.name` field. -- The image-reflector-controller scans the image repository for tags every hour, - indicated by the `.spec.interval` field. -- The registry authentication is done using a generic provider, indicated by the - `.spec.provider` field and referenced using `.spec.secretRef`. No - authentication is attempted when secret reference is not provided for generic - provider. See [Provider](#provider) for more details related to registry - authentication. -- The canonical form of the image set in `.spec.image` is used to scan the - repository. The resolved canonical form of the image is reported in the - `.status.canonicalImageName` field. -- The result of the scan is reported in the `.status.lastScanResult` field. - -This example can be run by saving the manifest into `imagerepository.yaml`. - -1. Apply the resource on the cluster: - -```sh -kubectl apply -f imagerepository.yaml -``` - -2. Run `kubectl get imagerepository` to see the ImageRepository: - -```console -NAME LAST SCAN TAGS -podinfo 2022-09-15T22:34:05Z 211 -``` - -3. Run `kubectl describe imagerepository podinfo` to see the [Last Scan Result](#last-scan-result) -and [Conditions](#conditions) in the ImageRepository's Status: - -```console - -... -Status: - Canonical Image Name: index.docker.io/stefanprodan/podinfo - Conditions: - Last Transition Time: 2022-09-15T22:38:42Z - Message: successful scan, found 211 tags - Observed Generation: 1 - Reason: Succeeded - Status: True - Type: Ready - Last Scan Result: - Latest Tags: - latest - 6.2.0 - 6.1.8 - 6.1.7 - 6.1.6 - 6.1.5 - 6.1.4 - 6.1.3 - 6.1.2 - 6.1.1 - Scan Time: 2022-09-15T22:38:42Z - Tag Count: 211 - Observed Exclusion List: - ^.*\.sig$ - Observed Generation: 1 -Events: - Type Reason Age From Message - ---- ------ ---- ---- ------- - Normal Succeeded 17s image-reflector-controller successful scan, found 211 tags -``` - -## Writing an ImageRepository spec - -As with all other Kubernetes config, an ImageRepository needs `apiVersion`, -`kind`, and `metadata` fields. The name of an ImageRepository object must be a -valid [DNS subdomain name](https://kubernetes.io/docs/concepts/overview/working-with-objects/names#dns-subdomain-names). - -An ImageRepository also needs a -[`.spec` section](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status). - -### Image - -`.spec.image` is a required field that specifies the address of an image -repository without any scheme prefix, e.g. `fluxcd/image-reflector-controller`. -This image is converted to its canonical form by the controller before scanning. -The canonical form of the image is reflected in `.status.canonicalImageName`. - -### Interval - -`.spec.interval` is a required field that specifies the interval at which the -Image repository must be scanned. - -After successfully reconciling the object, the image-reflector-controller -requeues it for inspection after the specified interval. The value must be in a -[Go recognized duration string format](https://pkg.go.dev/time#ParseDuration), -e.g. `10m0s` to reconcile the object every 10 minutes. - -If the `.metadata.generation` of a resource changes (due to e.g. a change to -the spec), this is handled instantly outside the interval window. - -### Timeout - -`.spec.timeout` is an optional field to specify a timeout for various operations -during the reconciliation like fetching the referred secrets, scanning the -repository, etc. The value must be in a -[Go recognized duration string format](https://pkg.go.dev/time#ParseDuration), -e.g. `1m30s` for a timeout of one minute and thirty seconds. The default value -is the value of `.spec.interval`. - -### Secret reference - -`.spec.secretRef.name` is an optional field to specify a name reference to a -Secret in the same namespace as the ImageRepository, containing authentication -credentials for the Image repository. The secret is expected to be in the same -format as the [docker config secrets](https://kubernetes.io/docs/concepts/configuration/secret/#docker-config-secrets), usually created by `kubectl create secret -docker-registry`. - -Example of using secret reference in an ImageRepository: - -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImageRepository -metadata: - name: podinfo - namespace: default -spec: - image: stefanprodan/podinfo - interval: 1h - secretRef: - name: regcred ---- -apiVersion: v1 -kind: Secret -metadata: - name: regcred - namespace: default -type: kubernetes.io/dockerconfigjson -data: - .dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsidXNlcm5hbWUiOiJmb28iLCJwYXNzd29yZCI6ImJhciIsImF1dGgiOiJabTl2T21KaGNnPT0ifX19 -``` - -For a publicly accessible image repository, there's no need to provide a secret -reference. - -### ServiceAccount name - -`.spec.serviceAccountName` is an optional field to specify a name reference to a -ServiceAccount in the same namespace as the ImageRepository, with an image pull -secret attached to it. For detailed instructions about attaching an image pull -secret to a ServiceAccount, see [Add image pull secret to service account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-image-pull-secret-to-service-account). - -### Certificate secret reference - -`.spec.certSecretRef` is an optional field to specify a name reference to a -Secret in the same namespace as the ImageRepository containing TLS certificate -data. This is for two separate purposes: -- to provide a client certificate and private key, if you use a certificate to authenticate with the image registry; and, -- to provide a CA certificate, if the registry uses a self-signed certificate - -These will often go together in case of self-hosted image registry. All the -files in the secret are expected to be [PEM-encoded][pem-encoding]. This is an -ASCII format for certificates and keys; `openssl` and such tools typically -provide an option for PEM output. - -Assuming that a certificate file and private key are in files `client.crt` and -`client.key` respectively, a secret can be created with `kubectl`: - -```sh -kubectl create secret generic tls-certs \ - --from-file=certFile=client.crt \ - --from-file=keyFile=client.key -``` - -An [encrypted secret](sops-guide) can also be used; the important bit is that -the data keys in the secret are `certFile` and `keyFile`. - -In case of a CA certificate for the client to use, the data key for it is -`caFile`. Adapting the previous example, if the certificate is in the file -`ca.crt`, and the client certificate and key are as before, the whole command -would be: - -```sh -kubectl create secret generic tls-certs \ - --from-file=certFile=client.crt \ - --from-file=keyFile=client.key \ - --from-file=caFile=ca.crt -``` - -### Suspend - -`.spec.suspend` is an optional field to suspend the reconciliation of an -ImageRepository. When set to `true`, the controller will stop reconciling the -ImageRepository, and changes to the resource or image repository will not result -in new scan results. When the field is set to `false` or removed, it will -resume. - -### Access from - -`.spec.accessFrom` is an optional field to restrict cross-namespace access of -ImageRepositories. To grant access to an ImageRepository for policies in other -namespaces, the owner of the ImageRepository has to specify a list of label -selectors that match the namespace labels of the ImagePolicy objects. - -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImageRepository -metadata: - name: app1 - namespace: apps -spec: - interval: 1h - image: docker.io/org/image - secretRef: - name: regcred - accessFrom: - namespaceSelectors: - - matchLabels: - kubernetes.io/metadata.name: flux-system -``` - -**Note:** The `kubernetes.io/metadata.name` label above is a readonly label -added by Kubernetes >= 1.21 automatically on namespaces. For older version of -Kubernetes, please set labels on the namespaces where the ImagePolicy exist. - -The above definition, allows ImagePolicy in the `flux-system` namespace to -reference the `app1` ImageRepository e.g.: - -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImagePolicy -metadata: - name: app1 - namespace: flux-system -spec: - imageRepositoryRef: - name: app1 - namespace: apps - policy: - semver: - range: 1.0.x -``` - -To grant access to all namespaces, an empty `matchLabels` can be set: - -```yaml - accessFrom: - namespaceSelectors: - - matchLabels: {} -``` - -### Exclusion list - -`.spec.exclusionList` is an optional field to exclude certain tags in the image -scan result. It's a list of regular expression patterns with a default value of -`"^.*\\.sig$"` if it's not set. This default value is used to exclude all the -tags ending with `.sig`, since these are [Cosign](https://github.com/sigstore/cosign) -generated objects and not container images which can be deployed on a Kubernetes -cluster. - -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImageRepository -metadata: - name: app1 - namespace: apps -spec: - interval: 1h - image: docker.io/org/image - exclusionList: - - "^.*\\.sig$" - - "1.0.2" - - "1.1.1|1.0.0" -``` - -### Provider - -`.spec.provider` is an optional field that allows specifying an OIDC provider -used for authentication purposes. - -Supported options are: - -- `generic` -- `aws` -- `azure` -- `gcp` - -The `generic` provider can be used for public repositories or when static -credentials are used for authentication, either with `.spec.secretRef` or -`.spec.serviceAccount`. If `.spec.provider` is not specified, it defaults to -`generic`. - -#### AWS - -The `aws` provider can be used to authenticate automatically using the EKS -worker node IAM role or IAM Role for Service Accounts (IRSA), and by extension -gain access to ECR. - -##### Worker Node IAM - -When the worker node IAM role has access to ECR, image-reflector-controller -running on it will also have access to ECR. Please take a look at this -[documentation](https://docs.aws.amazon.com/eks/latest/userguide/create-node-role.html) -for creating worker node IAM roles. - -##### IAM roles for service accounts(IRSA) - -When using IRSA to enable access to ECR, add the following patch to your -bootstrap repository, in the `flux-system/kustomization.yaml` file: - -```yaml -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - gotk-components.yaml - - gotk-sync.yaml -patches: - - patch: | - apiVersion: v1 - kind: ServiceAccount - metadata: - name: image-reflector-controller - annotations: - eks.amazonaws.com/role-arn: - target: - kind: ServiceAccount - name: image-reflector-controller -``` - -Note that you can attach the AWS managed policy `arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly` -to the IAM role when using IRSA and you have to configure the -`image-reflector-controller` to assume the IAM role. Please see -[documentation](https://docs.aws.amazon.com/eks/latest/userguide/associate-service-account-role.html). - -#### Azure - -The `azure` provider can be used to authenticate automatically using Workload -Identity, kubelet managed identity or Azure Active Directory pod-managed -identity (aad-pod-identity), and by extension gain access to ACR. - -##### Kubelet Identity - -When the kubelet managed identity has access to ACR, image-reflector-controller -running on it will also have access to ACR. - -##### Workload Identity - -When using workload identity to enable access to ACR, add the following patch to -properly annotate the image-reflector-controller pods and service account -in the `flux-system/kustomization.yaml` file: - -```yaml -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - gotk-components.yaml - - gotk-sync.yaml -patches: - - patch: |- - apiVersion: v1 - kind: ServiceAccount - metadata: - name: image-reflector-controller - namespace: flux-system - annotations: - azure.workload.identity/client-id: - labels: - azure.workload.identity/use: "true" - - patch: |- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: image-reflector-controller - namespace: flux-system - labels: - azure.workload.identity/use: "true" - spec: - template: - metadata: - labels: - azure.workload.identity/use: "true" -``` - -To use workload identity on your cluster, you would have to install workload -in your cluster, create an identity that has `AcrPull` role to ACR and establish -azure federated identity between the identity and the image-reflector-controller -service account. Please, take a look at the -[Azure documentation for Workload identity](https://azure.github.io/azure-workload-identity/docs/quick-start.html). - -##### AAD Pod Identity - -When using aad-pod-identity to enable access to ACR, add the following patch to -your bootstrap repository, in the `flux-system/kustomization.yaml` file: - -```yaml -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - gotk-components.yaml - - gotk-sync.yaml -patches: - - patch: | - - op: add - path: /spec/template/metadata/labels/aadpodidbinding - value: - target: - kind: Deployment - name: image-reflector-controller -``` - -When using pod-managed identity on an AKS cluster, AAD Pod Identity -has to be used to give the `image-reflector-controller` pod access to the ACR. -To do this, you have to install `aad-pod-identity` on your cluster, create a -managed identity that has access to the container registry (this can also be the -Kubelet identity if it has `AcrPull` role assignment on the ACR), create an -`AzureIdentity` and `AzureIdentityBinding` that describe the managed identity -and then label the `image-reflector-controller` pods with the name of the -AzureIdentity as shown in the patch above. Please take a look at -[this guide](https://azure.github.io/aad-pod-identity/docs/) or -[this one](https://docs.microsoft.com/en-us/azure/aks/use-azure-ad-pod-identity) -to use AKS pod-managed identities add-on that is in preview. - -#### GCP - -The `gcp` provider can be used to authenticate automatically using OAuth scopes -or Workload Identity, and by extension gain access to GCR or Artifact Registry. - -##### Access scopes - -When the GKE nodes have the appropriate OAuth scope for accessing GCR and -Artifact Registry, image-reflector-controller running on it will also have -access to them. - -##### Workload Identity - -When using Workload Identity to enable access to GCR or Artifact Registry, add -the following patch to your bootstrap repository, in the -`flux-system/kustomization.yaml` file: - -```yaml -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - gotk-components.yaml - - gotk-sync.yaml -patches: - - patch: | - apiVersion: v1 - kind: ServiceAccount - metadata: - name: image-reflector-controller - annotations: - iam.gke.io/gcp-service-account: - target: - kind: ServiceAccount - name: image-reflector-controller -``` - -The Artifact Registry service uses the permission `artifactregistry.repositories.downloadArtifacts` -that is located under the Artifact Registry Reader role. If you are using -Google Container Registry service, the needed permission is instead `storage.objects.list` -which can be bound as part of the Container Registry Service Agent role. -Take a look at [this guide](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) -for more information about setting up GKE Workload Identity. - -#### Authentication on other platforms - -For other platforms that link service permissions to service accounts, secret -can be created using tooling for that platform, rather than directly with -`kubectl create secret`. There is advice specific to some platforms in [the -image automation guide][image-auto-provider-secrets]. - -## Working with ImageRepositories - -### Triggering a reconcile - -To manually tell the image-reflector-controller to reconcile an ImageRepository -outside the [specified interval window](#interval), an ImageRepository can be -annotated with `reconcile.fluxcd.io/requestedAt: `. Annotating -the resource queues the ImageRepository for reconciliation if the -`` differs from the last value the controller acted on, as -reported in [`.status.lastHandledReconcileAt`](#last-handled-reconcile-at). - -Using `kubectl`: - -```sh -kubectl annotate --field-manager=flux-client-side-apply --overwrite imagerepository/ reconcile.fluxcd.io/requestedAt="$(date +%s)" -``` - -Using `flux`: - -```sh -flux reconcile image repository -``` - -### Waiting for `Ready` - -When a change is applied, it is possible to wait for the ImageRepository to -reach a [ready state](#ready-imagerepository) using `kubectl`: - -```sh -kubectl wait imagerepository/ --for=condition=ready --timeout=1m -``` - -### Suspending and resuming - -When you find yourself in a situation where you temporarily want to pause the -reconciliation of a ImageRepository, you can suspend it using the -[`.spec.suspend` field](#suspend). - -#### Suspend an ImageRepository - -In your YAML declaration: - -```yaml ---- -apiVersion: source.toolkit.fluxcd.io/v1beta3 -kind: ImageRepository -metadata: - name: -spec: - suspend: true -``` - -Using `kubectl`: - -```sh -kubectl patch imagerepository --field-manager=flux-client-side-apply -p '{\"spec\": {\"suspend\" : true }}' -``` - -Using `flux`: - -```sh -flux suspend image repository -``` - -**Note:** When an ImageRepository has scan results and is suspended, and this -result later disappears from the database due to e.g. the -image-reflector-controller Pod being evicted from a Node, this will not be -reflected in the ImageRepository's Status until it is resumed. - -#### Resume an ImageRepository - -In your YAML declaration, comment out (or remove) the `.spec.suspend` field: - -```yaml ---- -apiVersion: source.toolkit.fluxcd.io/v1beta3 -kind: ImageRepository -metadata: - name: -spec: - # suspend: true -``` - -**Note:** Setting the field value to `false` has the same effect as removing -it, but does not allow for "hot patching" using e.g. `kubectl` while practicing -GitOps; as the manually applied patch would be overwritten by the declared -state in Git. - -Using `kubectl`: - -```sh -kubectl patch imagerepository --field-manager=flux-client-side-apply -p '{\"spec\" : {\"suspend\" : false }}' -``` - -Using `flux`: - -```sh -flux resume image repository -``` - -### Debugging an ImageRepository - -There are several ways to gather information about an ImageRepository for -debugging purposes. - -#### Describe the ImageRepository - -Describing an ImageRepository using -`kubectl describe imagerepository ` -displays the latest recorded information for the resource in the `Status` and -`Events` sections: - -```console -... -Status: - Conditions: - Last Transition Time: 2022-09-19T05:47:40Z - Message: could not parse reference: ghcr.io/stefanprodan/podinfo:foo:bar - Observed Generation: 1 - Reason: ImageURLInvalid - Status: True - Type: Stalled - Last Transition Time: 2022-09-19T05:47:40Z - Message: could not parse reference: ghcr.io/stefanprodan/podinfo:foo:bar - Observed Generation: 1 - Reason: ImageURLInvalid - Status: False - Type: Ready - Observed Generation: 1 -Events: - Type Reason Age From Message - ---- ------ ---- ---- ------- - Warning ImageURLInvalid 5s image-reflector-controller could not parse reference: ghcr.io/stefanprodan/podinfo:foo:bar -``` - -#### Trace emitted Events - -To view events for specific ImageRepository(s), `kubectl events` can be used -in combination with `--for` to list the Events for specific objects. For -example, running - -```sh -kubectl events --for ImageRepository/ -``` - -lists - -```console -LAST SEEN TYPE REASON OBJECT MESSAGE -3m51s Normal Succeeded imagerepository/ successful scan, found 34 tags -114s Warning ImageURLInvalid imagerepository/ could not parse reference: ghcr.io/stefanprodan/podinfo:foo:bar -``` - -Besides being reported in Events, the reconciliation errors are also logged by -the controller. The Flux CLI offer commands for filtering the logs for a -specific ImageRepository, e.g. -`flux logs --level=error --kind=ImageRepository --name=`. - -## ImageRepository Status - -### Last Scan Result - -The ImageRepository reports the latest scanned tags from the image repository in -`.status.lastScanResult` for the resource. The tags are stored in an internal -database. `.status.lastScanResult.scanTime` shows the time of last scan. -`.status.lastScanResult.tagCount` shows the number of tags in the result. This -is calculated after applying any exclusion list rules. - -Example: -```yaml ---- -apiVersion: image.toolkit.fluxcd.io/v1beta3 -kind: ImageRepository -metadata: - name: -status: - lastScanResult: - latestTags: - - latest - - 6.2.0 - - 6.1.8 - - 6.1.7 - - 6.1.6 - - 6.1.5 - - 6.1.4 - - 6.1.3 - - 6.1.2 - - 6.1.1 - scanTime: "2022-09-19T05:53:27Z" - tagCount: 34 -``` - -### Canonical Image Name - -The ImageRepository reports the canonical form of the image repository provided -in the ImageRepository's `.spec.image` in `.status.canonicalImageName`. -Canonical name is the name of the image repository with all the implied bits -made explicit; e.g., `docker.io/library/alpine` rather than `alpine`. - -### Observed Exclusion List - -The ImageRepository reports an observed exclusion list in the ImageRepository's -`.status.observedExclusionList`. The observed exclusion list is the latest -`.spec.exclusionList` which resulted in a [ready state](#ready-imagerepository), -or stalled due to error it can not recover from without human intervention. - -### Conditions - -An ImageRepository enters various states during its lifecycle, reflected as -[Kubernetes Conditions][typical-status-properties]. -It can be [reconciling](#reconciling-imagerepository) while scanning the image -repository, it can be [ready](#ready-imagerepository), or it can [fail during -reconciliation](#failed-imagerepository). - -The ImageRepository API is compatible with the [kstatus specification][kstatus-spec], -and reports `Reconciling` and `Stalled` conditions where applicable to provide -better (timeout) support to solutions polling the ImageRepository to become -`Ready`. - -#### Reconciling ImageRepository - -The image-reflector-controller marks an ImageRepository as _reconciling_ when -one of the following is true: - -- The generation of the ImageRepository is newer than the [Observed -Generation](#observed-generation). -- The ImageRepository is being scanned because it's scan time as per the - specified `spec.interval`, or the ImageRepository has never been scanned - before, or the reported tags in the last scanned results have disappeared - from the database. - -When the ImageRepository is "reconciling", the `Ready` Condition status becomes -`False`, and the controller adds a Condition with the following attributes to -the ImageRepository's `.status.conditions`: - -- `type: Reconciling` -- `status: "True"` -- `reason: NewGeneration` | `reason: Scanning` - -It has a ["negative polarity"][typical-status-properties], and is only present -on the ImageRepository while its status value is `"True"`. - -#### Ready ImageRepository - -The image-reflector-controller marks an ImageRepository as _ready_ when it has -the following characteristics: - -- The ImageRepository reports a [Last Scan Result](#last-scan-result). -- The reported tags exists in the controller's internal database. -- The controller was able to communicate with the remote image repository using - the current spec. - -When the ImageRepository is "ready", the controller sets a Condition with the -following attributes in the ImageRepository's `.status.conditions`: - -- `type: Ready` -- `status: "True"` -- `reason: Succeeded` - -This `Ready` Condition will retain a status value of `"True"` until the -ImageRepository is marked as [reconciling](#reconciling-imagerepository), or -e.g. a [transient error](#failed-imagerepository) occurs due to a temporary -network issue. - -#### Failed ImageRepository - -The image-reflector-controller may get stuck trying to scan an image repository -without completing. This can occur due to some of the following factors: - -- The remote image repository is temporarily unavailable. -- The image repository does not exist. -- The [Secret reference](#secret-reference) and [Certificate secret reference](#certificate-secret-reference) - contains a reference to a non-existing Secret. -- The credentials and certificate in the referenced Secret are invalid. -- The ImageRepository spec contains a generic misconfiguration. -- A database related failure when reading or writing the scanned tags. - -When this happens, the controller sets the `Ready` Condition status to `False` -with the following reasons: - -- `reason: ImageURLInvalid` | `reason: AuthenticationFailed` | `reason: Failure` | `reason: ReadOperationFailed` - -While the ImageRepository is in failing state, the controller will continue to -attempt to scan the image repository for the resource with an exponential -backoff, until it succeeds and the ImageRepository is marked as -[ready](#ready-imagerepository). - -Note that an ImageRepository can be [reconciling](#reconciling-imagerepository) -while failing at the same time, for example due to a newly introduced -configuration issue in the ImageRepository spec. - -### Observed Generation - -The image-reflector-controller reports an -[observed generation][typical-status-properties] in the ImageRepository's -`.status.observedGeneration`. The observed generation is the latest -`.metadata.generation` which resulted in either a -[ready state](#ready-imagerepository), or stalled due to error it can not -recover from without human intervention. - -### Last Handled Reconcile At - -The image-reflector-controller reports the last -`reconcile.fluxcd.io/requestedAt` annotation value it acted on in the -`.status.lastHandledReconcileAt` field. - -For practical information about this field, see [triggering a -reconcile](#triggering-a-reconcile). - -[image-auto-provider-secrets]: https://fluxcd.io/flux/guides/image-update/#imagerepository-cloud-providers-authentication -[pem-encoding]: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail -[sops-guide]: https://fluxcd.io/flux/guides/mozilla-sops/ -[cloud providers authentication guide]: https://fluxcd.io/flux/guides/image-update/#imagerepository-cloud-providers-authentication -[typical-status-properties]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties -[kstatus-spec]: https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus diff --git a/internal/controller/controllers_fuzzer_test.go b/internal/controller/controllers_fuzzer_test.go index 9e471762..98c03df8 100644 --- a/internal/controller/controllers_fuzzer_test.go +++ b/internal/controller/controllers_fuzzer_test.go @@ -49,7 +49,7 @@ import ( fuzz "github.com/AdaLogics/go-fuzz-headers" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" "github.com/fluxcd/image-reflector-controller/internal/database" "github.com/fluxcd/image-reflector-controller/internal/test" ) diff --git a/internal/controller/imagepolicy_controller.go b/internal/controller/imagepolicy_controller.go index 39612b59..d0300066 100644 --- a/internal/controller/imagepolicy_controller.go +++ b/internal/controller/imagepolicy_controller.go @@ -48,7 +48,7 @@ import ( "github.com/fluxcd/pkg/runtime/patch" pkgreconcile "github.com/fluxcd/pkg/runtime/reconcile" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + 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" ) @@ -257,6 +257,7 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP } // Cleanup the last result. + obj.Status.LatestImage = "" obj.Status.LatestRef = imagev1.ImageRef{} // Get ImageRepository from reference. @@ -316,6 +317,7 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP } // Write the observations on status. + obj.Status.LatestImage = repo.Spec.Image + ":" + latest obj.Status.LatestRef.Name, obj.Status.LatestRef.Tag = repo.Spec.Image, latest // If the old latest image and new latest image don't match, set the old // image as the observed previous image. @@ -324,6 +326,7 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP // avoid creating an update event as there's no previous image to infer // from. Recovery from a failure shouldn't result in an update event. if oldObj.Status.LatestRef != obj.Status.LatestRef { + obj.Status.ObservedPreviousImage = oldObj.Status.LatestImage obj.Status.ObservedPreviousRef = oldObj.Status.LatestRef.DeepCopy() } diff --git a/internal/controller/imagepolicy_controller_test.go b/internal/controller/imagepolicy_controller_test.go index 0bbc257c..17e2d3e6 100644 --- a/internal/controller/imagepolicy_controller_test.go +++ b/internal/controller/imagepolicy_controller_test.go @@ -37,7 +37,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + 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" diff --git a/internal/controller/imagerepository_controller.go b/internal/controller/imagerepository_controller.go index 507717ad..f1156a34 100644 --- a/internal/controller/imagerepository_controller.go +++ b/internal/controller/imagerepository_controller.go @@ -47,7 +47,7 @@ import ( "github.com/fluxcd/pkg/runtime/predicates" "github.com/fluxcd/pkg/runtime/reconcile" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" "github.com/fluxcd/image-reflector-controller/internal/registry" ) diff --git a/internal/controller/imagerepository_controller_test.go b/internal/controller/imagerepository_controller_test.go index bcb8607f..b14efe15 100644 --- a/internal/controller/imagerepository_controller_test.go +++ b/internal/controller/imagerepository_controller_test.go @@ -33,7 +33,7 @@ import ( "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" "github.com/fluxcd/image-reflector-controller/internal/registry" "github.com/fluxcd/image-reflector-controller/internal/test" ) diff --git a/internal/controller/policy_test.go b/internal/controller/policy_test.go index 0ce537ef..070bda3b 100644 --- a/internal/controller/policy_test.go +++ b/internal/controller/policy_test.go @@ -35,7 +35,7 @@ import ( conditionscheck "github.com/fluxcd/pkg/runtime/conditions/check" "github.com/fluxcd/pkg/runtime/patch" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" "github.com/fluxcd/image-reflector-controller/internal/database" "github.com/fluxcd/image-reflector-controller/internal/test" // +kubebuilder:scaffold:imports diff --git a/internal/controller/scan_test.go b/internal/controller/scan_test.go index 864182d8..f7c999a3 100644 --- a/internal/controller/scan_test.go +++ b/internal/controller/scan_test.go @@ -37,7 +37,7 @@ import ( fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" "github.com/fluxcd/image-reflector-controller/internal/database" "github.com/fluxcd/image-reflector-controller/internal/test" // +kubebuilder:scaffold:imports diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index c1b92a9d..cf4dfd64 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -35,7 +35,7 @@ import ( "github.com/fluxcd/pkg/runtime/controller" "github.com/fluxcd/pkg/runtime/testenv" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + 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 diff --git a/internal/policy/factory.go b/internal/policy/factory.go index 4140a545..3d70140f 100644 --- a/internal/policy/factory.go +++ b/internal/policy/factory.go @@ -20,7 +20,7 @@ import ( "fmt" "strings" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) // PolicerFromSpec constructs a new policy object based on diff --git a/internal/policy/factory_test.go b/internal/policy/factory_test.go index f15f63f6..37d0428f 100644 --- a/internal/policy/factory_test.go +++ b/internal/policy/factory_test.go @@ -19,7 +19,7 @@ package policy import ( "testing" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) func TestFactory_PolicerFromSpec(t *testing.T) { diff --git a/internal/registry/helper.go b/internal/registry/helper.go index 92954774..af3b736e 100644 --- a/internal/registry/helper.go +++ b/internal/registry/helper.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + 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" diff --git a/internal/registry/helper_test.go b/internal/registry/helper_test.go index 1fb42294..b401a77e 100644 --- a/internal/registry/helper_test.go +++ b/internal/registry/helper_test.go @@ -28,7 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + 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" diff --git a/internal/registry/options.go b/internal/registry/options.go index 794ce220..06cdf5a1 100644 --- a/internal/registry/options.go +++ b/internal/registry/options.go @@ -14,7 +14,7 @@ import ( "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" "github.com/fluxcd/image-reflector-controller/internal/secret" ) diff --git a/main.go b/main.go index 56e92eab..10b834a9 100644 --- a/main.go +++ b/main.go @@ -48,7 +48,7 @@ import ( // +kubebuilder:scaffold:imports - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" "github.com/fluxcd/image-reflector-controller/internal/controller" "github.com/fluxcd/image-reflector-controller/internal/database" "github.com/fluxcd/image-reflector-controller/internal/features" diff --git a/tests/integration/imagerepo_test.go b/tests/integration/imagerepo_test.go index 01d9d97e..5ba0e574 100644 --- a/tests/integration/imagerepo_test.go +++ b/tests/integration/imagerepo_test.go @@ -25,7 +25,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) func TestImageRepositoryScan(t *testing.T) { diff --git a/tests/integration/suite_test.go b/tests/integration/suite_test.go index 910bd6cf..c9b37ac0 100644 --- a/tests/integration/suite_test.go +++ b/tests/integration/suite_test.go @@ -29,7 +29,7 @@ import ( tfjson "github.com/hashicorp/terraform-json" "k8s.io/apimachinery/pkg/runtime" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta3" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" "github.com/fluxcd/test-infra/tftestenv" ) From 99b5126a4417d90b697d9c487e2bc20a10d75405 Mon Sep 17 00:00:00 2001 From: Max Jonas Werner Date: Tue, 5 Sep 2023 14:13:52 +0200 Subject: [PATCH 05/17] Fix up print columns to reflect changes in ImagePolicy status Signed-off-by: Max Jonas Werner --- api/v1beta2/imagepolicy_types.go | 4 +++- .../crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/api/v1beta2/imagepolicy_types.go b/api/v1beta2/imagepolicy_types.go index be5fb88c..c96026b5 100644 --- a/api/v1beta2/imagepolicy_types.go +++ b/api/v1beta2/imagepolicy_types.go @@ -177,7 +177,9 @@ func (p *ImagePolicy) SetConditions(conditions []metav1.Condition) { // +kubebuilder:storageversion // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="LatestImage",type=string,JSONPath=`.status.latestImage` +// +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` // ImagePolicy is the Schema for the imagepolicies API type ImagePolicy struct { diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml index e670533a..dbb2243f 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml @@ -206,9 +206,15 @@ spec: subresources: status: {} - additionalPrinterColumns: - - jsonPath: .status.latestImage + - jsonPath: .status.latestRef.image name: LatestImage type: string + - jsonPath: .status.latestRef.tag + name: LatestTag + type: string + - jsonPath: .status.latestRef.digest + name: LatestDigest + type: string name: v1beta2 schema: openAPIV3Schema: From c85404e4e61d547906582a512c0c5da47b406eaa Mon Sep 17 00:00:00 2001 From: Max Jonas Werner Date: Tue, 5 Sep 2023 14:50:53 +0200 Subject: [PATCH 06/17] Fix comment Signed-off-by: Max Jonas Werner --- api/v1beta2/imagepolicy_types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1beta2/imagepolicy_types.go b/api/v1beta2/imagepolicy_types.go index c96026b5..be391e9a 100644 --- a/api/v1beta2/imagepolicy_types.go +++ b/api/v1beta2/imagepolicy_types.go @@ -42,7 +42,7 @@ type ImagePolicySpec struct { // ordered and compared. // +optional FilterTags *TagFilter `json:"filterTags,omitempty"` - // ReflectDigest governs the setting of the `.status.latestDigest` field. + // DigestReflectionPolicy governs the setting of the `.status.latestDigest` field. // +optional DigestReflectionPolicy *ReflectionPolicy `json:"digestReflectionPolicy,omitempty"` } From 7fa66fb4fd135cf076455acda447656cd61d97a1 Mon Sep 17 00:00:00 2001 From: Max Jonas Werner Date: Tue, 5 Sep 2023 14:53:58 +0200 Subject: [PATCH 07/17] Add missing license headers Signed-off-by: Max Jonas Werner --- internal/registry/helper.go | 16 ++++++++++++++++ internal/registry/options.go | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/internal/registry/helper.go b/internal/registry/helper.go index af3b736e..d3153946 100644 --- a/internal/registry/helper.go +++ b/internal/registry/helper.go @@ -1,3 +1,19 @@ +/* +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 import ( diff --git a/internal/registry/options.go b/internal/registry/options.go index 06cdf5a1..add04bcf 100644 --- a/internal/registry/options.go +++ b/internal/registry/options.go @@ -1,3 +1,19 @@ +/* +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 import ( From 3cd160b121e27c1bc5acff057e895b5b3c8c3d92 Mon Sep 17 00:00:00 2001 From: Max Jonas Werner Date: Tue, 5 Sep 2023 15:19:23 +0200 Subject: [PATCH 08/17] Address remaining review comments Signed-off-by: Max Jonas Werner --- ...image.toolkit.fluxcd.io_imagepolicies.yaml | 2 +- docs/api/v1beta2/image-reflector.md | 4 +- internal/controller/imagepolicy_controller.go | 10 +- .../controller/imagepolicy_controller_test.go | 2 +- .../controller/imagerepository_controller.go | 4 +- internal/controller/suite_test.go | 18 +- internal/registry/helper.go | 23 -- internal/registry/options.go | 207 ++++++++++-------- .../{helper_test.go => options_test.go} | 6 +- main.go | 28 +-- 10 files changed, 149 insertions(+), 155 deletions(-) rename internal/registry/{helper_test.go => options_test.go} (97%) diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml index dbb2243f..7d4d704b 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml @@ -237,7 +237,7 @@ spec: ImagePolicy. properties: digestReflectionPolicy: - description: ReflectDigest governs the setting of the `.status.latestDigest` + description: DigestReflectionPolicy governs the setting of the `.status.latestDigest` field. enum: - Always diff --git a/docs/api/v1beta2/image-reflector.md b/docs/api/v1beta2/image-reflector.md index bad64fb3..6af1ab27 100644 --- a/docs/api/v1beta2/image-reflector.md +++ b/docs/api/v1beta2/image-reflector.md @@ -142,7 +142,7 @@ ReflectionPolicy (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.

      @@ -301,7 +300,6 @@ ReflectionPolicy -(Optional)

      DigestReflectionPolicy governs the setting of the .status.latestDigest field.

      diff --git a/internal/controller/imagepolicy_controller.go b/internal/controller/imagepolicy_controller.go index 0713f3c9..e6103089 100644 --- a/internal/controller/imagepolicy_controller.go +++ b/internal/controller/imagepolicy_controller.go @@ -258,7 +258,7 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP // Cleanup the last result. obj.Status.LatestImage = "" - obj.Status.LatestRef = imagev1.ImageRef{} + obj.Status.LatestRef = nil // Get ImageRepository from reference. repo, err := r.getImageRepository(ctx, obj) @@ -318,7 +318,8 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP // Write the observations on status. obj.Status.LatestImage = repo.Spec.Image + ":" + latest - obj.Status.LatestRef.Name, obj.Status.LatestRef.Tag = repo.Spec.Image, latest + lr := imagev1.ImageRef{Name: repo.Spec.Image, Tag: latest} + obj.Status.LatestRef = &lr // If the old latest image and new latest image don't match, set the old // image as the observed previous image. // NOTE: The following allows the previous image to be set empty when @@ -358,12 +359,12 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP } func (r *ImagePolicyReconciler) updateDigest(ctx context.Context, repo *imagev1.ImageRepository, obj, oldObj *imagev1.ImagePolicy, tag string) error { - if obj.Spec.DigestReflectionPolicy == nil || strings.EqualFold(string(*obj.Spec.DigestReflectionPolicy), string(imagev1.ReflectNever)) { + if obj.GetDigestReflectionPolicy() == imagev1.ReflectNever { obj.Status.LatestRef.Digest = "" return nil } - if strings.EqualFold(string(*obj.Spec.DigestReflectionPolicy), string(imagev1.ReflectIfNotPresent)) && + if obj.GetDigestReflectionPolicy() == imagev1.ReflectIfNotPresent && oldObj.Status.LatestRef.Digest != "" && obj.Status.LatestRef.Name == oldObj.Status.LatestRef.Name && obj.Status.LatestRef.Tag == oldObj.Status.LatestRef.Tag { diff --git a/internal/controller/imagepolicy_controller_test.go b/internal/controller/imagepolicy_controller_test.go index 726db973..3a63faec 100644 --- a/internal/controller/imagepolicy_controller_test.go +++ b/internal/controller/imagepolicy_controller_test.go @@ -287,14 +287,14 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { tests := []struct { name string semVerPolicy2ndPass string - refPolicy1stPass *imagev1.ReflectionPolicy - refPolicy2ndPass *imagev1.ReflectionPolicy + refPolicy1stPass imagev1.ReflectionPolicy + refPolicy2ndPass imagev1.ReflectionPolicy digest1stPass func() string digest2ndPass func() string }{ { name: "missing policy leaves digest empty", - refPolicy1stPass: nil, + refPolicy1stPass: "", digest1stPass: func() string { return "" }, @@ -304,7 +304,7 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { }, { name: "'Never' policy leaves digest empty", - refPolicy1stPass: &polNever, + refPolicy1stPass: polNever, digest1stPass: func() string { return "" }, @@ -314,8 +314,8 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { }, { name: "'Always' policy always updates digest", - refPolicy1stPass: &polAlways, - refPolicy2ndPass: &polAlways, + refPolicy1stPass: polAlways, + refPolicy2ndPass: polAlways, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, @@ -326,8 +326,8 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { { name: "'IfNotPresent' policy updates digest when new tag is selected", semVerPolicy2ndPass: "v2.x", - refPolicy1stPass: &polIfNotPresent, - refPolicy2ndPass: &polIfNotPresent, + refPolicy1stPass: polIfNotPresent, + refPolicy2ndPass: polIfNotPresent, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, @@ -337,8 +337,8 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { }, { name: "'IfNotPresent' policy only sets digest once", - refPolicy1stPass: &polIfNotPresent, - refPolicy2ndPass: &polIfNotPresent, + refPolicy1stPass: polIfNotPresent, + refPolicy2ndPass: polIfNotPresent, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, @@ -348,8 +348,8 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { }, { name: "unsetting 'Always' policy removes digest", - refPolicy1stPass: &polAlways, - refPolicy2ndPass: &polNever, + refPolicy1stPass: polAlways, + refPolicy2ndPass: polNever, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, @@ -359,8 +359,8 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { }, { name: "unsetting 'IfNotPresent' policy removes digest", - refPolicy1stPass: &polIfNotPresent, - refPolicy2ndPass: &polNever, + refPolicy1stPass: polIfNotPresent, + refPolicy2ndPass: polNever, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, @@ -370,8 +370,8 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { }, { name: "changing 'IfNotPresent' to 'Always' sets new digest", - refPolicy1stPass: &polIfNotPresent, - refPolicy2ndPass: &polAlways, + refPolicy1stPass: polIfNotPresent, + refPolicy2ndPass: polAlways, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, @@ -381,8 +381,8 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { }, { name: "changing 'Always' to 'IfNotPresent' leaves digest untouched", - refPolicy1stPass: &polAlways, - refPolicy2ndPass: &polIfNotPresent, + refPolicy1stPass: polAlways, + refPolicy2ndPass: polIfNotPresent, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, diff --git a/internal/controller/policy_test.go b/internal/controller/policy_test.go index 070bda3b..598d91db 100644 --- a/internal/controller/policy_test.go +++ b/internal/controller/policy_test.go @@ -220,7 +220,7 @@ func TestImagePolicyReconciler_calculateImageFromRepoTags(t *testing.T) { g.Eventually(func() bool { err := testEnv.Get(ctx, polName, &pol) return err == nil && - pol.Status.LatestRef != imagev1.ImageRef{} + pol.Status.LatestRef != nil }, timeout, interval).Should(BeTrue()) g.Expect(pol.Status.LatestRef.String()).To(Equal(imgRepo + tt.wantImageTag)) } else { @@ -330,7 +330,7 @@ func TestImagePolicyReconciler_filterTags(t *testing.T) { if !tt.wantFailure { g.Eventually(func() bool { err := testEnv.Get(ctx, polName, &pol) - return err == nil && pol.Status.LatestRef != imagev1.ImageRef{} + return err == nil && pol.Status.LatestRef != nil }, timeout, interval).Should(BeTrue()) g.Expect(pol.Status.LatestRef.String()).To(Equal(imgRepo + tt.wantImageTag)) } else { @@ -506,7 +506,7 @@ func TestImagePolicyReconciler_accessImageRepo(t *testing.T) { if tt.wantAccessible { g.Eventually(func() bool { err := testEnv.Get(ctx, polName, &pol) - return err == nil && pol.Status.LatestRef.Name != "" + return err == nil && pol.Status.LatestRef != nil }, timeout, interval).Should(BeTrue()) g.Expect(pol.Status.LatestRef.String()).To(Equal(imgRepo + ":1.0.1")) } else { From b787e9c555bb0d7cf81816b2439bf1a6d36062f8 Mon Sep 17 00:00:00 2001 From: Max Jonas Werner Date: Fri, 15 Sep 2023 20:58:14 +0200 Subject: [PATCH 13/17] Add proper setting of .status.observedPreviousRef The field hasn't been set properly before. Correct behaviour is backed by associated unit tests. Signed-off-by: Max Jonas Werner --- internal/controller/imagepolicy_controller.go | 15 ++- .../controller/imagepolicy_controller_test.go | 119 +++++++++++++++--- internal/controller/policy_test.go | 5 + 3 files changed, 114 insertions(+), 25 deletions(-) diff --git a/internal/controller/imagepolicy_controller.go b/internal/controller/imagepolicy_controller.go index e6103089..907e3981 100644 --- a/internal/controller/imagepolicy_controller.go +++ b/internal/controller/imagepolicy_controller.go @@ -320,22 +320,24 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP obj.Status.LatestImage = repo.Spec.Image + ":" + latest lr := imagev1.ImageRef{Name: repo.Spec.Image, Tag: latest} obj.Status.LatestRef = &lr + + if err := r.updateDigest(ctx, repo, obj, oldObj, latest); err != nil { + result, retErr = ctrl.Result{}, err + return + } + // If the old latest image and new latest image don't match, set the old // image as the observed previous image. // NOTE: The following allows the previous image to be set empty when // there's a failure and a subsequent recovery from it. This behavior helps // avoid creating an update event as there's no previous image to infer // from. Recovery from a failure shouldn't result in an update event. - if oldObj.Status.LatestRef != obj.Status.LatestRef { + if oldObj.Status.LatestRef == nil || + *oldObj.Status.LatestRef != *obj.Status.LatestRef { obj.Status.ObservedPreviousImage = oldObj.Status.LatestImage obj.Status.ObservedPreviousRef = oldObj.Status.LatestRef.DeepCopy() } - if err := r.updateDigest(ctx, repo, obj, oldObj, 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.ObservedPreviousRef != nil { @@ -365,6 +367,7 @@ func (r *ImagePolicyReconciler) updateDigest(ctx context.Context, repo *imagev1. } if obj.GetDigestReflectionPolicy() == imagev1.ReflectIfNotPresent && + oldObj.Status.LatestRef != nil && oldObj.Status.LatestRef.Digest != "" && obj.Status.LatestRef.Name == oldObj.Status.LatestRef.Name && obj.Status.LatestRef.Tag == oldObj.Status.LatestRef.Tag { diff --git a/internal/controller/imagepolicy_controller_test.go b/internal/controller/imagepolicy_controller_test.go index 3a63faec..c3c5ffdb 100644 --- a/internal/controller/imagepolicy_controller_test.go +++ b/internal/controller/imagepolicy_controller_test.go @@ -270,9 +270,6 @@ func TestImagePolicyReconciler_getImageRepository(t *testing.T) { } func TestImagePolicyReconciler_digestReflection(t *testing.T) { - polAlways := imagev1.ReflectionPolicy("AlWaYs") - polIfNotPresent := imagev1.ReflectionPolicy("IfNoTpReSeNt") - polNever := imagev1.ReflectionPolicy("NeVeR") registryServer := test.NewRegistryServer() defer registryServer.Close() @@ -291,6 +288,7 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { refPolicy2ndPass imagev1.ReflectionPolicy digest1stPass func() string digest2ndPass func() string + previousRef2ndPass func() *imagev1.ImageRef }{ { name: "missing policy leaves digest empty", @@ -301,94 +299,174 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { digest2ndPass: func() string { return "" }, + previousRef2ndPass: func() *imagev1.ImageRef { + return nil + }, }, { name: "'Never' policy leaves digest empty", - refPolicy1stPass: polNever, + refPolicy1stPass: imagev1.ReflectNever, digest1stPass: func() string { return "" }, digest2ndPass: func() string { return "" }, + previousRef2ndPass: func() *imagev1.ImageRef { + return nil + }, }, { name: "'Always' policy always updates digest", - refPolicy1stPass: polAlways, - refPolicy2ndPass: polAlways, + refPolicy1stPass: imagev1.ReflectAlways, + refPolicy2ndPass: imagev1.ReflectAlways, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, digest2ndPass: func() string { return images2ndPass["v1.1.1"].String() }, + previousRef2ndPass: func() *imagev1.ImageRef { + return &imagev1.ImageRef{ + Name: imgRepo, + Tag: "v1.1.1", + Digest: images1stPass["v1.1.1"].String(), + } + }, }, { name: "'IfNotPresent' policy updates digest when new tag is selected", semVerPolicy2ndPass: "v2.x", - refPolicy1stPass: polIfNotPresent, - refPolicy2ndPass: polIfNotPresent, + refPolicy1stPass: imagev1.ReflectIfNotPresent, + refPolicy2ndPass: imagev1.ReflectIfNotPresent, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, digest2ndPass: func() string { return images2ndPass["v2.0.0"].String() }, + previousRef2ndPass: func() *imagev1.ImageRef { + return &imagev1.ImageRef{ + Name: imgRepo, + Tag: "v1.1.1", + Digest: images1stPass["v1.1.1"].String(), + } + }, }, { name: "'IfNotPresent' policy only sets digest once", - refPolicy1stPass: polIfNotPresent, - refPolicy2ndPass: polIfNotPresent, + refPolicy1stPass: imagev1.ReflectIfNotPresent, + refPolicy2ndPass: imagev1.ReflectIfNotPresent, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, digest2ndPass: func() string { return images1stPass["v1.1.1"].String() }, + previousRef2ndPass: func() *imagev1.ImageRef { + return nil + }, + }, + { + name: "changing 'Never' to 'IfNotPresent' sets observedPreviousRef correctly", + refPolicy1stPass: imagev1.ReflectNever, + refPolicy2ndPass: imagev1.ReflectIfNotPresent, + digest1stPass: func() string { + return "" + }, + digest2ndPass: func() string { + return images2ndPass["v1.1.1"].String() + }, + previousRef2ndPass: func() *imagev1.ImageRef { + return &imagev1.ImageRef{ + Name: imgRepo, + Tag: "v1.1.1", + Digest: "", + } + }, }, { name: "unsetting 'Always' policy removes digest", - refPolicy1stPass: polAlways, - refPolicy2ndPass: polNever, + refPolicy1stPass: imagev1.ReflectAlways, + refPolicy2ndPass: imagev1.ReflectNever, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, digest2ndPass: func() string { return "" }, + previousRef2ndPass: func() *imagev1.ImageRef { + return &imagev1.ImageRef{ + Name: imgRepo, + Tag: "v1.1.1", + Digest: images1stPass["v1.1.1"].String(), + } + }, }, { name: "unsetting 'IfNotPresent' policy removes digest", - refPolicy1stPass: polIfNotPresent, - refPolicy2ndPass: polNever, + refPolicy1stPass: imagev1.ReflectIfNotPresent, + refPolicy2ndPass: imagev1.ReflectNever, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, digest2ndPass: func() string { return "" }, + previousRef2ndPass: func() *imagev1.ImageRef { + return &imagev1.ImageRef{ + Name: imgRepo, + Tag: "v1.1.1", + Digest: images1stPass["v1.1.1"].String(), + } + }, }, { name: "changing 'IfNotPresent' to 'Always' sets new digest", - refPolicy1stPass: polIfNotPresent, - refPolicy2ndPass: polAlways, + refPolicy1stPass: imagev1.ReflectIfNotPresent, + refPolicy2ndPass: imagev1.ReflectAlways, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, digest2ndPass: func() string { return images2ndPass["v1.1.1"].String() }, + previousRef2ndPass: func() *imagev1.ImageRef { + return &imagev1.ImageRef{ + Name: imgRepo, + Tag: "v1.1.1", + Digest: images1stPass["v1.1.1"].String(), + } + }, }, { name: "changing 'Always' to 'IfNotPresent' leaves digest untouched", - refPolicy1stPass: polAlways, - refPolicy2ndPass: polIfNotPresent, + refPolicy1stPass: imagev1.ReflectAlways, + refPolicy2ndPass: imagev1.ReflectIfNotPresent, + digest1stPass: func() string { + return images1stPass["v1.1.1"].String() + }, + digest2ndPass: func() string { + return images1stPass["v1.1.1"].String() + }, + previousRef2ndPass: func() *imagev1.ImageRef { + return nil + }, + }, + { + name: "selecting same tag with different policy leaves observedPreviousRef empty", + refPolicy1stPass: imagev1.ReflectIfNotPresent, + semVerPolicy2ndPass: "=v1.1.1", + refPolicy2ndPass: imagev1.ReflectIfNotPresent, digest1stPass: func() string { return images1stPass["v1.1.1"].String() }, digest2ndPass: func() string { return images1stPass["v1.1.1"].String() }, + previousRef2ndPass: func() *imagev1.ImageRef { + return nil + }, }, } @@ -478,6 +556,8 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { g.Expect(imagePol.Status.LatestRef.Digest). To(Equal(tt.digest1stPass()), "unexpected 1st pass digest in status") + g.Expect(imagePol.Status.ObservedPreviousRef).To(BeNil(), + "observedPreviousRef should always be nil after a single pass") // Now, change the policy (if the test desires it) and overwrite the existing latest tag with a new image @@ -514,9 +594,10 @@ func TestImagePolicyReconciler_digestReflection(t *testing.T) { g.Expect( c.Get(context.Background(), client.ObjectKeyFromObject(imagePol), imagePol), ).To(Succeed(), "failed getting image policy") - g.Expect(imagePol.Status.LatestRef.Digest). To(Equal(tt.digest2ndPass()), "unexpected 2nd pass digest in status") + g.Expect(imagePol.Status.ObservedPreviousRef).To(Equal(tt.previousRef2ndPass()), + "unexpected content in .status.observedPreviousRef") }) } } diff --git a/internal/controller/policy_test.go b/internal/controller/policy_test.go index 598d91db..b8c9fcc6 100644 --- a/internal/controller/policy_test.go +++ b/internal/controller/policy_test.go @@ -223,6 +223,10 @@ func TestImagePolicyReconciler_calculateImageFromRepoTags(t *testing.T) { pol.Status.LatestRef != nil }, timeout, interval).Should(BeTrue()) g.Expect(pol.Status.LatestRef.String()).To(Equal(imgRepo + tt.wantImageTag)) + g.Expect(pol.Status.ObservedPreviousImage).To(Equal(""), + "single reconciliation should leave status.observedPreviousImage empty") + g.Expect(pol.Status.ObservedPreviousRef).To(BeNil(), + "single reconciliation should leave status.observedPreviousRef nil") } else { g.Eventually(func() bool { err := testEnv.Get(ctx, polName, &pol) @@ -238,6 +242,7 @@ func TestImagePolicyReconciler_calculateImageFromRepoTags(t *testing.T) { checker.WithT(g).CheckErr(ctx, &pol) g.Expect(testEnv.Delete(ctx, &pol)).To(Succeed()) + g.Expect(testEnv.Delete(ctx, &repo)).To(Succeed()) }) } } From 5cfa7665b6d794e3687842889098a1d50b3b03c0 Mon Sep 17 00:00:00 2001 From: Max Jonas Werner Date: Fri, 15 Sep 2023 22:01:17 +0200 Subject: [PATCH 14/17] Bring back updated spec docs The updated documentation has gotten lost due to the back and forth with v1beta3. Signed-off-by: Max Jonas Werner --- docs/spec/v1beta2/imagepolicies.md | 48 +++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/docs/spec/v1beta2/imagepolicies.md b/docs/spec/v1beta2/imagepolicies.md index 8b73621f..d5eeb0ee 100644 --- a/docs/spec/v1beta2/imagepolicies.md +++ b/docs/spec/v1beta2/imagepolicies.md @@ -21,6 +21,7 @@ metadata: spec: imageRepositoryRef: name: podinfo + digestReflectionPolicy: IfNotPresent policy: semver: range: 5.1.x @@ -37,8 +38,8 @@ In the above example: the scanned tags from the internal database for the image name. The read tags 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`. +- The latest image's name is derived from the ImageRepository image and reported + together with the selected tag and digest in the `.status.latestRef` object. This example can be run by saving the manifest into `imagepolicy.yaml`. @@ -68,6 +69,10 @@ Status: Status: True Type: Ready Latest Image: ghcr.io/stefanprodan/podinfo:5.1.4 + Latest Ref: + Digest: sha256:2d9a00b3981628a533ff43352193b1838b0a4bf6b0033444286f563205e51a2c + Image: ghcr.io/stefanprodan/podinfo + Tag: 5.1.4 Observed Generation: 1 Events: Type Reason Age From Message @@ -250,6 +255,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 a field that governs the reflection of the selected image's +digest in the ImagePolicy's `.status.latestRef.digest` field. The field has three possible values: + +- `Never`: If the field is set to `Never` (the default) 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.latestRef.digest`. 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 @@ -331,10 +349,12 @@ specific ImagePolicy, e.g. ## ImagePolicy Status -### Latest Image +### Latest Ref -The ImagePolicy reports the latest select image from the ImageRepository tags in -`.status.latestImage` for the resource. +The ImagePolicy reports the latest selected image from the ImageRepository tags in +`.status.latestRef` for the resource. The field `.status.latestRef.digest` is dependent +on the [chosen digest reflection policy](#digest-reflection) and is only set for the +`Always` or `IfNotPresent` policies. Example: @@ -345,13 +365,17 @@ kind: ImagePolicy metadata: name: status: - latestImage: ghcr.io/stefanprodan/podinfo:5.1.4 + latestRef: + image: ghcr.io/stefanprodan/podinfo + tag: 5.1.4 + digest: sha256:2d9a00b3981628a533ff43352193b1838b0a4bf6b0033444286f563205e51a2c +[...] ``` -### Observed Previous Image +### Observed Previous Ref The ImagePolicy reports the previously observed latest image in -`.status.observedPreviousImage` for the resource. This is used by the +`.status.observedPreviousRef` for the resource. This is used by the ImagePolicy to determine an upgrade path of an ImagePolicy update. This field is reset when the ImagePolicy fails due to some reason to be able to distinguish between a failure recovery and a genuine latest image upgrade. @@ -364,8 +388,12 @@ kind: ImagePolicy metadata: name: status: - latestImage: ghcr.io/stefanprodan/podinfo:6.2.1 - observedPreviousImage: ghcr.io/stefanprodan/podinfo:5.1.4 + latestRef: + image: ghcr.io/stefanprodan/podinfo + tag: 6.2.1 + observedPreviousRef: + image: ghcr.io/stefanprodan/podinfo + tag: 5.1.4 ``` ### Conditions From 6c9b87eb2fd7456d5ad40b29da2be28e218330a0 Mon Sep 17 00:00:00 2001 From: Max Jonas Werner Date: Mon, 2 Oct 2023 18:50:45 +0200 Subject: [PATCH 15/17] Address latest PR comments Signed-off-by: Max Jonas Werner --- api/v1beta2/imagepolicy_types.go | 2 +- docs/spec/v1beta2/imagepolicies.md | 43 +++++++++ internal/controller/imagepolicy_controller.go | 2 +- .../controller/imagepolicy_controller_test.go | 89 +++++++++++++++++++ 4 files changed, 134 insertions(+), 2 deletions(-) diff --git a/api/v1beta2/imagepolicy_types.go b/api/v1beta2/imagepolicy_types.go index 2bb6cc1a..59ff96c1 100644 --- a/api/v1beta2/imagepolicy_types.go +++ b/api/v1beta2/imagepolicy_types.go @@ -42,7 +42,7 @@ type ImagePolicySpec struct { // ordered and compared. // +optional FilterTags *TagFilter `json:"filterTags,omitempty"` - // DigestReflectionPolicy governs the setting of the `.status.latestDigest` field. + // DigestReflectionPolicy governs the setting of the `.status.latestRef.digest` field. // +kubebuilder:default:=Never DigestReflectionPolicy ReflectionPolicy `json:"digestReflectionPolicy,omitempty"` } diff --git a/docs/spec/v1beta2/imagepolicies.md b/docs/spec/v1beta2/imagepolicies.md index d5eeb0ee..f51167f0 100644 --- a/docs/spec/v1beta2/imagepolicies.md +++ b/docs/spec/v1beta2/imagepolicies.md @@ -349,6 +349,49 @@ specific ImagePolicy, e.g. ## ImagePolicy Status +### Latest Image + +**Warning:** This field is deprecated in favor of `.status.latestRef.image` and will be +removed in a future release. + +The ImagePolicy reports the latest select image from the ImageRepository tags in +`.status.latestImage` for the resource. + +Example: + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta2 +kind: ImagePolicy +metadata: + name: +status: + latestImage: ghcr.io/stefanprodan/podinfo:5.1.4 +``` + +### Observed Previous Image + +**Warning:** This field is deprecated in favor of `.status.observedPreviousRef.image` +and will be removed in a future release. + +The ImagePolicy reports the previously observed latest image in +`.status.observedPreviousImage` for the resource. This is used by the +ImagePolicy to determine an upgrade path of an ImagePolicy update. This field +is reset when the ImagePolicy fails due to some reason to be able to distinguish +between a failure recovery and a genuine latest image upgrade. + +Example: + +```yaml +apiVersion: image.toolkit.fluxcd.io/v1beta2 +kind: ImagePolicy +metadata: + name: +status: + latestImage: ghcr.io/stefanprodan/podinfo:6.2.1 + observedPreviousImage: ghcr.io/stefanprodan/podinfo:5.1.4 +``` + ### Latest Ref The ImagePolicy reports the latest selected image from the ImageRepository tags in diff --git a/internal/controller/imagepolicy_controller.go b/internal/controller/imagepolicy_controller.go index 907e3981..1c000bdf 100644 --- a/internal/controller/imagepolicy_controller.go +++ b/internal/controller/imagepolicy_controller.go @@ -332,7 +332,7 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP // there's a failure and a subsequent recovery from it. This behavior helps // avoid creating an update event as there's no previous image to infer // from. Recovery from a failure shouldn't result in an update event. - if oldObj.Status.LatestRef == nil || + if oldObj.Status.LatestRef != nil && *oldObj.Status.LatestRef != *obj.Status.LatestRef { obj.Status.ObservedPreviousImage = oldObj.Status.LatestImage obj.Status.ObservedPreviousRef = oldObj.Status.LatestRef.DeepCopy() diff --git a/internal/controller/imagepolicy_controller_test.go b/internal/controller/imagepolicy_controller_test.go index c3c5ffdb..1afad417 100644 --- a/internal/controller/imagepolicy_controller_test.go +++ b/internal/controller/imagepolicy_controller_test.go @@ -79,6 +79,95 @@ func TestImagePolicyReconciler_deleteBeforeFinalizer(t *testing.T) { g.Expect(err).NotTo(HaveOccurred()) } +func TestStatusMigrationToImageRef(t *testing.T) { + g := NewWithT(t) + + s := runtime.NewScheme() + utilruntime.Must(imagev1.AddToScheme(s)) + utilruntime.Must(corev1.AddToScheme(s)) + + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "imagepolicy-" + randStringRunes(5), + }, + } + + imageRepo := &imagev1.ImageRepository{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "status-migration-test", + }, + Spec: imagev1.ImageRepositorySpec{ + Image: "ghcr.io/stefanprodan/podinfo", + }, + Status: imagev1.ImageRepositoryStatus{ + LastScanResult: &imagev1.ScanResult{ + TagCount: 3, + LatestTags: []string{"1.0.0", "1.1.0", "2.0.0"}, + }, + }, + } + imagePol := &imagev1.ImagePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "status-migration-test", + Generation: 1, + Finalizers: []string{imagev1.ImageFinalizer}, + }, + Spec: imagev1.ImagePolicySpec{ + ImageRepositoryRef: meta.NamespacedObjectReference{ + Name: imageRepo.Name, + }, + Policy: imagev1.ImagePolicyChoice{ + SemVer: &imagev1.SemVerPolicy{ + Range: "1.0", + }, + }, + }, + Status: imagev1.ImagePolicyStatus{ + LatestImage: "ghcr.io/stefanprodan/podinfo:1.0.0", + }, + } + + c := fake.NewClientBuilder(). + WithScheme(s). + WithObjects(ns, imageRepo, imagePol). + WithStatusSubresource(imagePol). + Build() + + r := &ImagePolicyReconciler{ + EventRecorder: record.NewFakeRecorder(32), + Client: c, + Database: &mockDatabase{TagData: imageRepo.Status.LastScanResult.LatestTags}, + AuthOptionsGetter: registry.NewAuthOptionsGetter(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.LatestImage).To(Equal("ghcr.io/stefanprodan/podinfo:1.0.0"), "unexpected latest image") + g.Expect(imagePol.Status.LatestRef).To(Equal(&imagev1.ImageRef{ + Name: "ghcr.io/stefanprodan/podinfo", + Tag: "1.0.0", + Digest: "", + }), "unexpected latest ref") + g.Expect(imagePol.Status.ObservedPreviousImage).To(BeEmpty(), "unexpected observed previous image") + g.Expect(imagePol.Status.ObservedPreviousRef).To(BeNil(), "unexpected observed previous ref") +} + func TestImagePolicyReconciler_getImageRepository(t *testing.T) { testImageRepoName := "test-repo" testNamespace1 := "test-ns1" // Default namespace of ImagePolicy. From 962576b617d5df7486059f065ee78393a5923326 Mon Sep 17 00:00:00 2001 From: Max Jonas Werner Date: Mon, 2 Oct 2023 22:56:41 +0200 Subject: [PATCH 16/17] Generate manifests Signed-off-by: Max Jonas Werner --- config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml index 0d0e877a..d7981507 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml @@ -232,7 +232,7 @@ spec: properties: digestReflectionPolicy: default: Never - description: DigestReflectionPolicy governs the setting of the `.status.latestDigest` + description: DigestReflectionPolicy governs the setting of the `.status.latestRef.digest` field. enum: - Always From 13c2f709d5042849d729d806d74f9bdd456956ac Mon Sep 17 00:00:00 2001 From: Max Jonas Werner Date: Wed, 4 Oct 2023 09:06:16 +0200 Subject: [PATCH 17/17] Re-generate API docs Signed-off-by: Max Jonas Werner --- docs/api/v1beta2/image-reflector.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/v1beta2/image-reflector.md b/docs/api/v1beta2/image-reflector.md index 1e3233a7..e83d27da 100644 --- a/docs/api/v1beta2/image-reflector.md +++ b/docs/api/v1beta2/image-reflector.md @@ -141,7 +141,7 @@ ReflectionPolicy -

      DigestReflectionPolicy governs the setting of the .status.latestDigest field.

      +

      DigestReflectionPolicy governs the setting of the .status.latestRef.digest field.

      @@ -300,7 +300,7 @@ ReflectionPolicy -

      DigestReflectionPolicy governs the setting of the .status.latestDigest field.

      +

      DigestReflectionPolicy governs the setting of the .status.latestRef.digest field.