Skip to content

Commit

Permalink
Prevent downgrade to a major/minor version
Browse files Browse the repository at this point in the history
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
  • Loading branch information
stefanprodan committed May 29, 2024
1 parent 47ac2a7 commit cef9177
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 26 deletions.
2 changes: 1 addition & 1 deletion config/samples/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## Append samples of your project ##
resources:
- fluxcd_v1alpha1_fluxinstance.yaml
- fluxcd_v1_fluxinstance.yaml
# +kubebuilder:scaffold:manifestskustomizesamples
27 changes: 27 additions & 0 deletions internal/builder/semver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/fluxinstance_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 103 additions & 0 deletions internal/controller/fluxinstance_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
`,
},
},
},
}
}
25 changes: 0 additions & 25 deletions internal/controller/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
`,
},
},
},
}
}

0 comments on commit cef9177

Please sign in to comment.