diff --git a/api/v1/fluxinstance_types.go b/api/v1/fluxinstance_types.go index 2c1f228..b5c74d9 100644 --- a/api/v1/fluxinstance_types.go +++ b/api/v1/fluxinstance_types.go @@ -243,6 +243,12 @@ func (in *FluxInstance) SetConditions(conditions []metav1.Condition) { in.Status.Conditions = conditions } +// IsDisabled returns true if the object has the reconcile annotation set to 'disabled'. +func (in *FluxInstance) IsDisabled() bool { + val, ok := in.GetAnnotations()[ReconcileAnnotation] + return ok && strings.ToLower(val) == DisabledValue +} + // 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 { diff --git a/internal/controller/fluxinstance_controller.go b/internal/controller/fluxinstance_controller.go index 3d89bac..2c4a3c2 100644 --- a/internal/controller/fluxinstance_controller.go +++ b/internal/controller/fluxinstance_controller.go @@ -5,6 +5,7 @@ package controller import ( "context" + "errors" "fmt" "os" "path/filepath" @@ -89,6 +90,14 @@ func (r *FluxInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{Requeue: true}, nil } + // Pause reconciliation if the object has the reconcile annotation set to 'disabled'. + if obj.IsDisabled() { + msg := "Reconciliation in disabled" + log.Error(errors.New("can't reconcile instance"), msg) + r.Event(obj, corev1.EventTypeWarning, "ReconciliationDisabled", msg) + return ctrl.Result{}, nil + } + // Reconcile the object. return r.reconcile(ctx, obj, patcher) } diff --git a/internal/controller/fluxinstance_controller_test.go b/internal/controller/fluxinstance_controller_test.go index 25f2934..e60e43b 100644 --- a/internal/controller/fluxinstance_controller_test.go +++ b/internal/controller/fluxinstance_controller_test.go @@ -325,6 +325,106 @@ func TestFluxInstanceReconciler_Downgrade(t *testing.T) { g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) } +func TestFluxInstanceReconciler_Disabled(t *testing.T) { + g := NewWithT(t) + reconciler := getFluxInstanceReconciler() + spec := getDefaultFluxSpec() + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + ns, err := testEnv.CreateNamespace(ctx, "test") + g.Expect(err).ToNot(HaveOccurred()) + + obj := &fluxcdv1.FluxInstance{ + ObjectMeta: metav1.ObjectMeta{ + Name: ns.Name, + Namespace: ns.Name, + }, + Spec: spec, + } + + err = testClient.Create(ctx, obj) + g.Expect(err).ToNot(HaveOccurred()) + + // Initialize the instance. + r, err := reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: client.ObjectKeyFromObject(obj), + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(r.Requeue).To(BeTrue()) + + // Install the instance. + r, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: client.ObjectKeyFromObject(obj), + }) + g.Expect(err).ToNot(HaveOccurred()) + + // Check if the instance was installed. + result := &fluxcdv1.FluxInstance{} + err = testClient.Get(ctx, client.ObjectKeyFromObject(obj), result) + g.Expect(err).ToNot(HaveOccurred()) + checkInstanceReadiness(g, result) + + // Disable the instance reconciliation. + resultP := result.DeepCopy() + resultP.SetAnnotations( + map[string]string{ + fluxcdv1.ReconcileAnnotation: fluxcdv1.DisabledValue, + }) + resultP.Spec.Components = []fluxcdv1.Component{"source-controller"} + err = testClient.Patch(ctx, resultP, client.MergeFrom(result)) + g.Expect(err).ToNot(HaveOccurred()) + + // Reconcile the instance with disabled reconciliation. + r, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: client.ObjectKeyFromObject(obj), + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(r.IsZero()).To(BeTrue()) + + // Check the final status. + resultFinal := &fluxcdv1.FluxInstance{} + err = testClient.Get(ctx, client.ObjectKeyFromObject(obj), resultFinal) + g.Expect(err).ToNot(HaveOccurred()) + + // Check if events were recorded for each step. + events := getEvents(result.Name) + g.Expect(events).To(HaveLen(3)) + g.Expect(events[0].Reason).To(Equal(meta.ProgressingReason)) + g.Expect(events[1].Reason).To(Equal(meta.ReconciliationSucceededReason)) + g.Expect(events[2].Reason).To(Equal("ReconciliationDisabled")) + + // Check that resources were not deleted. + kc := &appsv1.Deployment{} + err = testClient.Get(ctx, types.NamespacedName{Name: "kustomize-controller", Namespace: ns.Name}, kc) + g.Expect(err).ToNot(HaveOccurred()) + + // Enable the instance reconciliation. + resultP = resultFinal.DeepCopy() + resultP.SetAnnotations( + map[string]string{ + fluxcdv1.ReconcileAnnotation: fluxcdv1.EnabledValue, + }) + err = testClient.Patch(ctx, resultP, client.MergeFrom(result)) + g.Expect(err).ToNot(HaveOccurred()) + + // Uninstall the instance. + err = testClient.Delete(ctx, obj) + g.Expect(err).ToNot(HaveOccurred()) + + r, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: client.ObjectKeyFromObject(obj), + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(r.IsZero()).To(BeTrue()) + + // Check that resources were not deleted. + sc := &appsv1.Deployment{} + err = testClient.Get(ctx, types.NamespacedName{Name: "source-controller", Namespace: ns.Name}, sc) + g.Expect(err).To(HaveOccurred()) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) +} + func TestFluxInstanceReconciler_Profiles(t *testing.T) { g := NewWithT(t) reconciler := getFluxInstanceReconciler() diff --git a/internal/controller/fluxinstance_uninstaller.go b/internal/controller/fluxinstance_uninstaller.go index 6f3d392..2179964 100644 --- a/internal/controller/fluxinstance_uninstaller.go +++ b/internal/controller/fluxinstance_uninstaller.go @@ -30,7 +30,7 @@ func (r *FluxInstanceReconciler) uninstall(ctx context.Context, reconcileStart := time.Now() log := ctrl.LoggerFrom(ctx) - if obj.Status.Inventory == nil || len(obj.Status.Inventory.Entries) == 0 { + if obj.IsDisabled() || obj.Status.Inventory == nil || len(obj.Status.Inventory.Entries) == 0 { controllerutil.RemoveFinalizer(obj, fluxcdv1.Finalizer) return ctrl.Result{}, nil }