diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 438fa77..5977452 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -1,4 +1,4 @@ ## Append samples of your project ## resources: -- fluxcd_v1alpha1_fluxinstance.yaml +- fluxcd_v1_fluxinstance.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/internal/builder/semver.go b/internal/builder/semver.go index 667cc48..78cecc5 100644 --- a/internal/builder/semver.go +++ b/internal/builder/semver.go @@ -8,10 +8,37 @@ import ( "os" "path/filepath" "sort" + "strings" "github.com/Masterminds/semver/v3" ) +// IsCompatibleVersion checks if the version upgrade is compatible. +// It returns an error if a downgrade to a lower minor version is attempted. +func IsCompatibleVersion(fromVer, toVer string) error { + if strings.Contains(fromVer, "@") { + fromVer = strings.Split(fromVer, "@")[0] + } + from, err := semver.NewVersion(fromVer) + if err != nil { + return fmt.Errorf("from version '%s' parse error: %w", fromVer, err) + } + + if strings.Contains(toVer, "@") { + toVer = strings.Split(toVer, "@")[0] + } + to, err := semver.NewVersion(toVer) + if err != nil { + return fmt.Errorf("to version '%s' parse error: %w", toVer, err) + } + + if to.Major() < from.Major() || to.Minor() < from.Minor() { + return fmt.Errorf("downgrading from %s to %s is not supported, reinstall needed", fromVer, toVer) + } + + return nil +} + // MatchVersion returns the latest version dir path that matches the given semver range. func MatchVersion(dataDir, semverRange string) (string, error) { if _, err := os.Stat(dataDir); os.IsNotExist(err) { diff --git a/internal/controller/fluxinstance_controller.go b/internal/controller/fluxinstance_controller.go index c45c63f..0b98b67 100644 --- a/internal/controller/fluxinstance_controller.go +++ b/internal/controller/fluxinstance_controller.go @@ -183,6 +183,12 @@ func (r *FluxInstanceReconciler) build(ctx context.Context, return nil, err } + if obj.Status.LastAppliedRevision != "" { + if err := builder.IsCompatibleVersion(obj.Status.LastAppliedRevision, ver); err != nil { + return nil, err + } + } + options := builder.MakeDefaultOptions() options.Version = ver options.Registry = obj.GetDistribution().Registry diff --git a/internal/controller/fluxinstance_controller_test.go b/internal/controller/fluxinstance_controller_test.go index 0a35a61..6abec07 100644 --- a/internal/controller/fluxinstance_controller_test.go +++ b/internal/controller/fluxinstance_controller_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/fluxcd/pkg/apis/kustomize" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" . "github.com/onsi/gomega" @@ -233,6 +234,84 @@ func TestFluxInstanceReconciler_InstallFail(t *testing.T) { g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) } +func TestFluxInstanceReconciler_Downgrade(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) + + // Try to downgrade. + resultP := result.DeepCopy() + resultP.Spec.Distribution.Version = "v2.2.x" + err = testClient.Patch(ctx, resultP, client.MergeFrom(result)) + g.Expect(err).ToNot(HaveOccurred()) + + r, err = reconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: client.ObjectKeyFromObject(obj), + }) + g.Expect(err).ToNot(HaveOccurred()) + + // Check the final status. + resultFinal := &fluxcdv1.FluxInstance{} + err = testClient.Get(ctx, client.ObjectKeyFromObject(obj), resultFinal) + g.Expect(err).ToNot(HaveOccurred()) + + // Check if the downgraded was rejected. + logObjectStatus(t, resultFinal) + g.Expect(conditions.IsStalled(resultFinal)).To(BeTrue()) + g.Expect(conditions.GetMessage(resultFinal, meta.ReadyCondition)).To(ContainSubstring("is not supported")) + + // 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 if the instance was uninstalled. + 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() @@ -305,3 +384,27 @@ func TestFluxInstanceReconciler_Profiles(t *testing.T) { g.Expect(err).To(HaveOccurred()) g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) } + +func getDefaultFluxSpec() fluxcdv1.FluxInstanceSpec { + return fluxcdv1.FluxInstanceSpec{ + Wait: false, + Distribution: fluxcdv1.Distribution{ + Version: "v2.3.x", + Registry: "ghcr.io/fluxcd", + }, + Kustomize: &fluxcdv1.Kustomize{ + Patches: []kustomize.Patch{ + { + Target: &kustomize.Selector{ + Kind: "Deployment", + }, + Patch: ` +- op: replace + path: /spec/replicas + value: 0 +`, + }, + }, + }, + } +} diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index c2fbb78..cb0ca34 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -12,7 +12,6 @@ import ( "time" "github.com/fluxcd/cli-utils/pkg/kstatus/polling" - "github.com/fluxcd/pkg/apis/kustomize" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" kcheck "github.com/fluxcd/pkg/runtime/conditions/check" @@ -119,27 +118,3 @@ func getEvents(objName string) []corev1.Event { } return result } - -func getDefaultFluxSpec() fluxcdv1.FluxInstanceSpec { - return fluxcdv1.FluxInstanceSpec{ - Wait: false, - Distribution: fluxcdv1.Distribution{ - Version: "*", - Registry: "ghcr.io/fluxcd", - }, - Kustomize: &fluxcdv1.Kustomize{ - Patches: []kustomize.Patch{ - { - Target: &kustomize.Selector{ - Kind: "Deployment", - }, - Patch: ` -- op: replace - path: /spec/replicas - value: 0 -`, - }, - }, - }, - } -}