diff --git a/README.md b/README.md index 3500dee..150f838 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ metadata: namespace: flux-system annotations: fluxcd.controlplane.io/reconcileEvery: "1h" + fluxcd.controlplane.io/reconcileArtifactEvery: "10m" fluxcd.controlplane.io/reconcileTimeout: "5m" spec: distribution: diff --git a/api/v1/fluxinstance_types.go b/api/v1/fluxinstance_types.go index e54e0c2..dae9d99 100644 --- a/api/v1/fluxinstance_types.go +++ b/api/v1/fluxinstance_types.go @@ -24,12 +24,13 @@ const ( ) var ( - Finalizer = fmt.Sprintf("%s/finalizer", GroupVersion.Group) - ReconcileAnnotation = fmt.Sprintf("%s/reconcile", GroupVersion.Group) - ReconcileEveryAnnotation = fmt.Sprintf("%s/reconcileEvery", GroupVersion.Group) - ReconcileTimeoutAnnotation = fmt.Sprintf("%s/reconcileTimeout", GroupVersion.Group) - PruneAnnotation = fmt.Sprintf("%s/prune", GroupVersion.Group) - RevisionAnnotation = fmt.Sprintf("%s/revision", GroupVersion.Group) + Finalizer = fmt.Sprintf("%s/finalizer", GroupVersion.Group) + ReconcileAnnotation = fmt.Sprintf("%s/reconcile", GroupVersion.Group) + ReconcileEveryAnnotation = fmt.Sprintf("%s/reconcileEvery", GroupVersion.Group) + ReconcileArtifactEveryAnnotation = fmt.Sprintf("%s/reconcileArtifactEvery", GroupVersion.Group) + ReconcileTimeoutAnnotation = fmt.Sprintf("%s/reconcileTimeout", GroupVersion.Group) + PruneAnnotation = fmt.Sprintf("%s/prune", GroupVersion.Group) + RevisionAnnotation = fmt.Sprintf("%s/revision", GroupVersion.Group) ) // FluxInstanceSpec defines the desired state of FluxInstance @@ -366,12 +367,29 @@ func (in *FluxInstance) IsDisabled() bool { // GetInterval returns the interval at which the object should be reconciled. // If no interval is set, the default is 60 minutes. func (in *FluxInstance) GetInterval() time.Duration { - val, ok := in.GetAnnotations()[ReconcileAnnotation] - if ok && strings.ToLower(val) == DisabledValue { + if in.IsDisabled() { return 0 } defaultInterval := 60 * time.Minute - val, ok = in.GetAnnotations()[ReconcileEveryAnnotation] + val, ok := in.GetAnnotations()[ReconcileEveryAnnotation] + if !ok { + return defaultInterval + } + interval, err := time.ParseDuration(val) + if err != nil { + return defaultInterval + } + return interval +} + +// GetArtifactInterval returns the interval at which the distribution artifact should be reconciled. +// If no interval is set, the default is 10 minutes. +func (in *FluxInstance) GetArtifactInterval() time.Duration { + if in.IsDisabled() { + return 0 + } + defaultInterval := 10 * time.Minute + val, ok := in.GetAnnotations()[ReconcileArtifactEveryAnnotation] if !ok { return defaultInterval } diff --git a/cmd/main.go b/cmd/main.go index 08a0f1d..38ae285 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -162,6 +162,18 @@ func main() { os.Exit(1) } + if err = (&controller.FluxInstanceArtifactReconciler{ + Client: mgr.GetClient(), + StatusManager: controllerName, + EventRecorder: mgr.GetEventRecorderFor(controllerName), + }).SetupWithManager(mgr, + controller.FluxInstanceArtifactReconcilerOptions{ + RateLimiter: runtimeCtrl.GetRateLimiter(rateLimiterOptions), + }); err != nil { + setupLog.Error(err, "unable to create controller", "controller", fluxcdv1.FluxInstanceKind+"Artifact") + os.Exit(1) + } + if err = (&controller.FluxReportReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/docs/api/v1/fluxinstance.md b/docs/api/v1/fluxinstance.md index 7eb3b17..b59e591 100644 --- a/docs/api/v1/fluxinstance.md +++ b/docs/api/v1/fluxinstance.md @@ -432,6 +432,7 @@ The reconciliation behaviour can be configured using the following annotations: - `fluxcd.controlplane.io/reconcile`: Enable or disable the reconciliation loop. Default is `enabled`, set to `disabled` to pause the reconciliation. - `fluxcd.controlplane.io/reconcileEvery`: Set the reconciliation interval. Default is `1h`. +- `fluxcd.controlplane.io/reconcileArtifactEvery`: Set the artifact reconciliation interval. Default is `10m`. - `fluxcd.controlplane.io/reconcileTimeout`: Set the reconciliation timeout. Default is `5m`. ### Sync configuration diff --git a/internal/builder/digest.go b/internal/builder/digest.go new file mode 100644 index 0000000..8132ce5 --- /dev/null +++ b/internal/builder/digest.go @@ -0,0 +1,21 @@ +// Copyright 2024 Stefan Prodan. +// SPDX-License-Identifier: AGPL-3.0 + +package builder + +import ( + "context" + "fmt" + "strings" + + "github.com/google/go-containerregistry/pkg/crane" +) + +// GetArtifactDigest looks up an artifact from an OCI repository and returns the digest of the artifact. +func GetArtifactDigest(ctx context.Context, ociURL string) (string, error) { + digest, err := crane.Digest(strings.TrimPrefix(ociURL, "oci://"), crane.WithContext(ctx)) + if err != nil { + return "", fmt.Errorf("fetching digest for artifact %s failed: %w", ociURL, err) + } + return digest, nil +} diff --git a/internal/controller/fluxinstance_artifact_controller.go b/internal/controller/fluxinstance_artifact_controller.go new file mode 100644 index 0000000..6629aa9 --- /dev/null +++ b/internal/controller/fluxinstance_artifact_controller.go @@ -0,0 +1,107 @@ +// Copyright 2024 Stefan Prodan. +// SPDX-License-Identifier: AGPL-3.0 + +package controller + +import ( + "context" + "fmt" + "time" + + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/conditions" + "github.com/fluxcd/pkg/runtime/patch" + corev1 "k8s.io/api/core/v1" + kuberecorder "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + fluxcdv1 "github.com/controlplaneio-fluxcd/flux-operator/api/v1" + "github.com/controlplaneio-fluxcd/flux-operator/internal/builder" +) + +// FluxInstanceArtifactReconciler reconciles the distribution artifact of a FluxInstance object +type FluxInstanceArtifactReconciler struct { + client.Client + kuberecorder.EventRecorder + + StatusManager string +} + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *FluxInstanceArtifactReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) { + obj := &fluxcdv1.FluxInstance{} + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Skip reconciliation if the object is under deletion. + if !obj.ObjectMeta.DeletionTimestamp.IsZero() { + return ctrl.Result{}, nil + } + + // Skip reconciliation if the object has the reconcile annotation set to 'disabled'. + if obj.IsDisabled() { + return ctrl.Result{}, nil + } + + // Skip reconciliation if the object does not have a last artifact revision to avoid race condition. + if obj.Status.LastArtifactRevision == "" { + return requeueArtifactAfter(obj), nil + } + + // Skip reconciliation if the object is not ready. + if !conditions.IsReady(obj) { + return requeueArtifactAfter(obj), nil + } + + // Reconcile the object. + patcher := patch.NewSerialPatcher(obj, r.Client) + return r.reconcile(ctx, obj, patcher) +} + +func (r *FluxInstanceArtifactReconciler) reconcile(ctx context.Context, + obj *fluxcdv1.FluxInstance, + patcher *patch.SerialPatcher) (ctrl.Result, error) { + + log := ctrl.LoggerFrom(ctx) + + // Fetch the latest digest of the distribution manifests. + artifactURL := obj.Spec.Distribution.Artifact + artifactDigest, err := builder.GetArtifactDigest(ctx, artifactURL) + if err != nil { + msg := fmt.Sprintf("fetch failed: %s", err.Error()) + r.Event(obj, corev1.EventTypeWarning, meta.ArtifactFailedReason, msg) + return ctrl.Result{}, err + } + log.V(1).Info("fetched latest manifests digest", "url", artifactURL, "digest", artifactDigest) + + // Skip reconciliation if the artifact has not changed. + if artifactDigest == obj.Status.LastArtifactRevision { + return requeueArtifactAfter(obj), nil + } + + // The digest has changed, request a reconciliation. + log.Info("artifact revision changed, requesting a reconciliation", + "old", obj.Status.LastArtifactRevision, "new", artifactDigest) + if obj.Annotations == nil { + obj.Annotations = make(map[string]string, 1) + } + obj.Annotations[meta.ReconcileRequestAnnotation] = time.Now().Format(time.RFC3339Nano) + if err := patcher.Patch(ctx, obj, patch.WithFieldOwner(r.StatusManager)); err != nil { + return ctrl.Result{}, err + } + + return requeueArtifactAfter(obj), nil +} + +// requeueArtifactAfter returns a ctrl.Result with the requeue time set to the +// interval specified in the object's annotation for artifact reconciliation. +func requeueArtifactAfter(obj *fluxcdv1.FluxInstance) ctrl.Result { + result := ctrl.Result{} + if d := obj.GetArtifactInterval(); d > 0 { + result.RequeueAfter = d + } + return result +} diff --git a/internal/controller/fluxinstance_artifact_controller_test.go b/internal/controller/fluxinstance_artifact_controller_test.go new file mode 100644 index 0000000..129ce83 --- /dev/null +++ b/internal/controller/fluxinstance_artifact_controller_test.go @@ -0,0 +1,184 @@ +// Copyright 2024 Stefan Prodan. +// SPDX-License-Identifier: AGPL-3.0 + +package controller + +import ( + "context" + "errors" + "strings" + "testing" + "time" + + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/conditions" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + fluxcdv1 "github.com/controlplaneio-fluxcd/flux-operator/api/v1" + "github.com/controlplaneio-fluxcd/flux-operator/internal/builder" +) + +func TestFluxInstanceArtifactReconciler(t *testing.T) { + const ( + cpLatestManifestsURL = "oci://ghcr.io/controlplaneio-fluxcd/flux-operator-manifests:latest" + outdatedArtifactRevision = "sha256:1234567890" + ) + + g := NewWithT(t) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + latestArtifactRevision, err := builder.GetArtifactDigest(ctx, cpLatestManifestsURL) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(latestArtifactRevision).To(HavePrefix("sha256:")) + g.Expect(strings.TrimPrefix(latestArtifactRevision, "sha256:")).To(HaveLen(64)) + + for _, tt := range []struct { + name string + delete bool + annotations map[string]string + manifestsURL string + notReady bool + lastArtifactRevision string + result ctrl.Result + err error + shouldRequestReconciliation bool + }{ + { + name: "requests reconciliation when digest is different", + manifestsURL: cpLatestManifestsURL, + lastArtifactRevision: outdatedArtifactRevision, + result: ctrl.Result{RequeueAfter: 10 * time.Minute}, + shouldRequestReconciliation: true, + }, + { + name: "does not request reconciliation when up-to-date", + manifestsURL: cpLatestManifestsURL, + lastArtifactRevision: latestArtifactRevision, + result: ctrl.Result{RequeueAfter: 10 * time.Minute}, + shouldRequestReconciliation: false, + }, + { + name: "uses interval from annotation", + annotations: map[string]string{"fluxcd.controlplane.io/reconcileArtifactEvery": "2m"}, + manifestsURL: cpLatestManifestsURL, + lastArtifactRevision: latestArtifactRevision, + result: ctrl.Result{RequeueAfter: 2 * time.Minute}, + shouldRequestReconciliation: false, + }, + { + name: "does not request reconciliation when on deletion", + delete: true, + manifestsURL: cpLatestManifestsURL, + lastArtifactRevision: outdatedArtifactRevision, + result: ctrl.Result{}, + shouldRequestReconciliation: false, + }, + { + name: "does not request reconciliation when disabled", + annotations: map[string]string{"fluxcd.controlplane.io/reconcile": "disabled"}, + manifestsURL: cpLatestManifestsURL, + lastArtifactRevision: outdatedArtifactRevision, + result: ctrl.Result{}, + shouldRequestReconciliation: false, + }, + { + name: "does not request reconciliation when last artifact revision is missing to avoid race condition", + manifestsURL: cpLatestManifestsURL, + lastArtifactRevision: "", + result: ctrl.Result{RequeueAfter: 10 * time.Minute}, + shouldRequestReconciliation: false, + }, + { + name: "does not request reconciliation when the object is not ready", + notReady: true, + manifestsURL: cpLatestManifestsURL, + lastArtifactRevision: outdatedArtifactRevision, + result: ctrl.Result{RequeueAfter: 10 * time.Minute}, + shouldRequestReconciliation: false, + }, + { + name: "does not request reconciliation on artifact error", + manifestsURL: "oci://not.found/artifact", + lastArtifactRevision: outdatedArtifactRevision, + result: ctrl.Result{}, + err: errors.New("no such host"), + shouldRequestReconciliation: false, + }, + } { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + reconciler := getFluxInstanceArtifactReconciler() + + ns, err := testEnv.CreateNamespace(ctx, "test") + g.Expect(err).ToNot(HaveOccurred()) + + obj := &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Name: ns.Name, + Namespace: ns.Name, + Annotations: tt.annotations, + }, + Spec: getDefaultFluxSpec(), + } + obj.Spec.Distribution.Artifact = tt.manifestsURL + + err = testEnv.Create(ctx, obj) + g.Expect(err).ToNot(HaveOccurred()) + + if tt.notReady { + conditions.MarkUnknown(obj, meta.ReadyCondition, + meta.ProgressingReason, "Reconciliation in progress") + } else { + conditions.MarkTrue(obj, meta.ReadyCondition, meta.ReconciliationSucceededReason, + "Reconciliation finished in %s", fmtDuration(time.Now())) + } + obj.Status.LastArtifactRevision = tt.lastArtifactRevision + err = testEnv.Status().Update(ctx, obj) + g.Expect(err).ToNot(HaveOccurred()) + + if tt.delete { + obj.Finalizers = append(obj.Finalizers, "test") + err := testEnv.Update(ctx, obj) + g.Expect(err).ToNot(HaveOccurred()) + err = testEnv.Delete(ctx, obj) + g.Expect(err).ToNot(HaveOccurred()) + } + + r, err := reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: client.ObjectKeyFromObject(obj), + }) + if tt.err != nil { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tt.err.Error())) + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + g.Expect(r).To(Equal(tt.result)) + + err = testEnv.Get(ctx, client.ObjectKeyFromObject(obj), obj) + g.Expect(err).ToNot(HaveOccurred()) + + annotations := obj.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + reconcileRequestAnnotation := annotations[meta.ReconcileRequestAnnotation] + + if tt.shouldRequestReconciliation { + g.Expect(reconcileRequestAnnotation).ToNot(BeEmpty()) + requestedAt, err := time.Parse(time.RFC3339Nano, reconcileRequestAnnotation) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(requestedAt).To(BeTemporally("~", time.Now(), time.Second)) + } else { + g.Expect(reconcileRequestAnnotation).To(BeEmpty()) + } + }) + } +} diff --git a/internal/controller/fluxinstance_artifact_manager.go b/internal/controller/fluxinstance_artifact_manager.go new file mode 100644 index 0000000..39e338f --- /dev/null +++ b/internal/controller/fluxinstance_artifact_manager.go @@ -0,0 +1,61 @@ +// Copyright 2024 Stefan Prodan. +// SPDX-License-Identifier: AGPL-3.0 + +package controller + +import ( + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + fluxcdv1 "github.com/controlplaneio-fluxcd/flux-operator/api/v1" +) + +// FluxInstanceArtifactReconcilerOptions contains options for the reconciler. +type FluxInstanceArtifactReconcilerOptions struct { + RateLimiter workqueue.TypedRateLimiter[reconcile.Request] +} + +// ArtifactReconciliationConfigurationChangedPredicate contains the logic +// to determine if the annotations of a FluxInstance object relevant to the +// artifact reconciliation have changed. +type ArtifactReconciliationConfigurationChangedPredicate struct { + predicate.Funcs +} + +// SetupWithManager sets up the controller with the Manager. +func (r *FluxInstanceArtifactReconciler) SetupWithManager(mgr ctrl.Manager, opts FluxInstanceArtifactReconcilerOptions) error { + return ctrl.NewControllerManagedBy(mgr). + Named("fluxinstance_artifact"). + Watches(&fluxcdv1.FluxInstance{}, + &handler.EnqueueRequestForObject{}, + builder.WithPredicates(ArtifactReconciliationConfigurationChangedPredicate{})). + WithOptions(controller.Options{ + RateLimiter: opts.RateLimiter, + }).Complete(r) +} + +func (ArtifactReconciliationConfigurationChangedPredicate) Update(e event.UpdateEvent) bool { + if e.ObjectOld == nil || e.ObjectNew == nil { + return false + } + + // Start reconciliation if the object was disabled and is now enabled. + oldObj := e.ObjectOld.(*fluxcdv1.FluxInstance) + newObj := e.ObjectNew.(*fluxcdv1.FluxInstance) + if oldObj.IsDisabled() && !newObj.IsDisabled() { + return true + } + + // Trigger reconciliation if the artifact interval has changed. + if oldObj.GetArtifactInterval() != newObj.GetArtifactInterval() { + return true + } + + return false +} diff --git a/internal/controller/fluxinstance_artifact_manager_test.go b/internal/controller/fluxinstance_artifact_manager_test.go new file mode 100644 index 0000000..14d21dd --- /dev/null +++ b/internal/controller/fluxinstance_artifact_manager_test.go @@ -0,0 +1,211 @@ +// Copyright 2024 Stefan Prodan. +// SPDX-License-Identifier: AGPL-3.0 + +package controller + +import ( + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + + fluxcdv1 "github.com/controlplaneio-fluxcd/flux-operator/api/v1" +) + +func TestArtifactAnnotationsChangedPredicate_Update(t *testing.T) { + for _, tt := range []struct { + name string + oldObj client.Object + newObj client.Object + result bool + }{ + { + name: "false if old object is nil", + oldObj: nil, + newObj: &fluxcdv1.FluxInstance{}, + result: false, + }, + { + name: "false if new object is nil", + oldObj: &fluxcdv1.FluxInstance{}, + newObj: nil, + result: false, + }, + { + name: "true if object was disabled and is now enabled", + oldObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/reconcile": "disabled", + }, + }, + }, + newObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/reconcile": "enabled", + }, + }, + }, + result: true, + }, + { + name: "true if object was disabled and no longer has the reconcile annotation", + oldObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/reconcile": "disabled", + }, + }, + }, + newObj: &fluxcdv1.FluxInstance{}, + result: true, + }, + { + name: "true if artifact interval changed", + oldObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/reconcileArtifactEvery": "20m", + }, + }, + }, + newObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/reconcileArtifactEvery": "2m", + }, + }, + }, + result: true, + }, + { + name: "true if artifact interval changed adding annotation", + oldObj: &fluxcdv1.FluxInstance{}, + newObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/reconcileArtifactEvery": "2m", + }, + }, + }, + result: true, + }, + { + name: "true if artifact interval changed removing annotation", + oldObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/reconcileArtifactEvery": "2m", + }, + }, + }, + newObj: &fluxcdv1.FluxInstance{}, + result: true, + }, + { + name: "false if unrelated annotation was removed", + oldObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/unrelated": "SomeValue", + }, + }, + }, + newObj: &fluxcdv1.FluxInstance{}, + result: false, + }, + { + name: "false if unrelated annotation was added", + oldObj: &fluxcdv1.FluxInstance{}, + newObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/unrelated": "SomeValue", + }, + }, + }, + result: false, + }, + { + name: "false if unrelated annotation changed", + oldObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/unrelated": "SomeValue", + }, + }, + }, + newObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/unrelated": "SomeOtherValue", + }, + }, + }, + result: false, + }, + { + name: "false if the artifact interval annotation with the default value was added", + oldObj: &fluxcdv1.FluxInstance{}, + newObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/reconcileArtifactEvery": "10m", + }, + }, + }, + result: false, + }, + { + name: "false if the artifact interval annotation with the default value was removed", + oldObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/reconcileArtifactEvery": "10m", + }, + }, + }, + newObj: &fluxcdv1.FluxInstance{}, + result: false, + }, + { + name: "false if the reconcile annotation with the default value was added", + oldObj: &fluxcdv1.FluxInstance{}, + newObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/reconcile": "enabled", + }, + }, + }, + result: false, + }, + { + name: "false if the reconcile annotation with the default value was removed", + oldObj: &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "fluxcd.controlplane.io/reconcile": "enabled", + }, + }, + }, + newObj: &fluxcdv1.FluxInstance{}, + result: false, + }, + } { + t.Run(tt.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + predicate := ArtifactReconciliationConfigurationChangedPredicate{} + + result := predicate.Update(event.UpdateEvent{ + ObjectOld: tt.oldObj, + ObjectNew: tt.newObj, + }) + g.Expect(result).To(Equal(tt.result)) + }) + } +} diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 2516cc1..18460c3 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -95,6 +95,14 @@ func getFluxInstanceReconciler() *FluxInstanceReconciler { } } +func getFluxInstanceArtifactReconciler() *FluxInstanceArtifactReconciler { + return &FluxInstanceArtifactReconciler{ + Client: testClient, + EventRecorder: testEnv.GetEventRecorderFor(controllerName), + StatusManager: controllerName, + } +} + func logObjectStatus(t *testing.T, obj client.Object) { u, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) status, _, _ := unstructured.NestedFieldCopy(u, "status")