diff --git a/controllers/istio/istio_controller.go b/controllers/istio/istio_controller.go index b492f39e4..126961a3b 100644 --- a/controllers/istio/istio_controller.go +++ b/controllers/istio/istio_controller.go @@ -22,6 +22,7 @@ import ( "strings" "time" + "github.com/Masterminds/semver/v3" "github.com/go-logr/logr" "github.com/istio-ecosystem/sail-operator/api/v1alpha1" "github.com/istio-ecosystem/sail-operator/pkg/config" @@ -98,6 +99,13 @@ func validate(istio *v1alpha1.Istio) error { if istio.Spec.Version == "" { return reconciler.NewValidationError("spec.version not set") } + istioVersion, err := semver.NewVersion(istio.Spec.Version) + if err != nil { + return reconciler.NewValidationError("spec.version is not a valid semver: " + err.Error()) + } + if config.Config.MaximumIstioVersion != nil && istioVersion.GreaterThan(config.Config.MaximumIstioVersion) { + return reconciler.NewValidationError("spec.version is not supported") + } if istio.Spec.Namespace == "" { return reconciler.NewValidationError("spec.namespace not set") } diff --git a/controllers/istio/istio_controller_test.go b/controllers/istio/istio_controller_test.go index 129acf0ea..067d7f90d 100644 --- a/controllers/istio/istio_controller_test.go +++ b/controllers/istio/istio_controller_test.go @@ -21,6 +21,7 @@ import ( "testing" "time" + "github.com/Masterminds/semver/v3" "github.com/google/go-cmp/cmp" "github.com/istio-ecosystem/sail-operator/api/v1alpha1" "github.com/istio-ecosystem/sail-operator/pkg/config" @@ -169,6 +170,7 @@ func TestValidate(t *testing.T) { name string istio *v1alpha1.Istio expectErr string + config config.OperatorConfig }{ { name: "success", @@ -207,8 +209,36 @@ func TestValidate(t *testing.T) { }, expectErr: "spec.namespace not set", }, + { + name: "invalid version", + istio: &v1alpha1.Istio{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + }, + Spec: v1alpha1.IstioSpec{ + Version: "v.-1.0", + }, + }, + expectErr: "spec.version is not a valid semver", + }, + { + name: "version higher than maximum istio version", + istio: &v1alpha1.Istio{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + }, + Spec: v1alpha1.IstioSpec{ + Version: "v2.1.0", + }, + }, + expectErr: "spec.version is not supported", + config: config.OperatorConfig{ + MaximumIstioVersion: semver.MustParse("v2.0.0"), + }, + }, } for _, tc := range testCases { + config.Config = tc.config t.Run(tc.name, func(t *testing.T) { g := NewWithT(t) diff --git a/controllers/istiorevision/istiorevision_controller.go b/controllers/istiorevision/istiorevision_controller.go index 716fa530c..cc30f872d 100644 --- a/controllers/istiorevision/istiorevision_controller.go +++ b/controllers/istiorevision/istiorevision_controller.go @@ -22,6 +22,7 @@ import ( "reflect" "regexp" + "github.com/Masterminds/semver/v3" "github.com/go-logr/logr" "github.com/istio-ecosystem/sail-operator/api/v1alpha1" "github.com/istio-ecosystem/sail-operator/pkg/config" @@ -123,6 +124,13 @@ func (r *Reconciler) validate(ctx context.Context, rev *v1alpha1.IstioRevision) if rev.Spec.Version == "" { return reconciler.NewValidationError("spec.version not set") } + istioVersion, err := semver.NewVersion(rev.Spec.Version) + if err != nil { + return reconciler.NewValidationError("spec.version is not a valid semver: " + err.Error()) + } + if config.Config.MaximumIstioVersion != nil && istioVersion.GreaterThan(config.Config.MaximumIstioVersion) { + return reconciler.NewValidationError("spec.version is not supported") + } if rev.Spec.Namespace == "" { return reconciler.NewValidationError("spec.namespace not set") } diff --git a/controllers/istiorevision/istiorevision_controller_test.go b/controllers/istiorevision/istiorevision_controller_test.go index 7c2f21b97..3282203fc 100644 --- a/controllers/istiorevision/istiorevision_controller_test.go +++ b/controllers/istiorevision/istiorevision_controller_test.go @@ -20,6 +20,7 @@ import ( "strings" "testing" + "github.com/Masterminds/semver/v3" "github.com/istio-ecosystem/sail-operator/api/v1alpha1" "github.com/istio-ecosystem/sail-operator/pkg/config" "github.com/istio-ecosystem/sail-operator/pkg/constants" @@ -52,6 +53,7 @@ func TestValidate(t *testing.T) { rev *v1alpha1.IstioRevision objects []client.Object expectErr string + config config.OperatorConfig }{ { name: "success", @@ -185,9 +187,53 @@ func TestValidate(t *testing.T) { objects: []client.Object{ns}, expectErr: `spec.values.revision does not match IstioRevision name`, }, + { + name: "invalid version", + rev: &v1alpha1.IstioRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-revision", + }, + Spec: v1alpha1.IstioRevisionSpec{ + Version: "v.-1.0", + Namespace: "istio-system", + Values: &v1alpha1.Values{ + Revision: ptr.Of("other-revision"), + Global: &v1alpha1.GlobalConfig{ + IstioNamespace: ptr.Of("other-namespace"), + }, + }, + }, + }, + objects: []client.Object{ns}, + expectErr: "spec.version is not a valid semver", + }, + { + name: "version higher than maximum istio version", + rev: &v1alpha1.IstioRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-revision", + }, + Spec: v1alpha1.IstioRevisionSpec{ + Version: "v2.1.0", + Namespace: "istio-system", + Values: &v1alpha1.Values{ + Revision: ptr.Of("other-revision"), + Global: &v1alpha1.GlobalConfig{ + IstioNamespace: ptr.Of("other-namespace"), + }, + }, + }, + }, + objects: []client.Object{ns}, + expectErr: "spec.version is not supported", + config: config.OperatorConfig{ + MaximumIstioVersion: semver.MustParse("v2.0.0"), + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + config.Config = tc.config g := NewWithT(t) cl := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(tc.objects...).Build() r := NewReconciler(cfg, cl, scheme.Scheme, nil) diff --git a/pkg/config/config.go b/pkg/config/config.go index 6d56c9a55..314cf4560 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -17,13 +17,15 @@ package config import ( "strings" + "github.com/Masterminds/semver/v3" "github.com/magiconair/properties" ) var Config = OperatorConfig{} type OperatorConfig struct { - ImageDigests map[string]IstioImageConfig `properties:"images"` + ImageDigests map[string]IstioImageConfig `properties:"images"` + MaximumIstioVersion *semver.Version `properties:"-"` // property name is 'maxIstioVersion' } type IstioImageConfig struct { @@ -59,5 +61,13 @@ func Read(configFile string) error { newImageDigests[strings.Replace(k, "_", ".", -1)] = v } Config.ImageDigests = newImageDigests + // special handling to decode maxIstioVersion field + maxIstioVersion := p.GetString("maxIstioVersion", "") + if maxIstioVersion != "" { + Config.MaximumIstioVersion, err = semver.NewVersion(maxIstioVersion) + if err != nil { + return err + } + } return nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index d34716ac4..0fbeece84 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -18,6 +18,7 @@ import ( "os" "testing" + "github.com/Masterminds/semver/v3" "github.com/google/go-cmp/cmp" ) @@ -84,6 +85,24 @@ images.v1_20_0.ztunnel=ztunnel-test `, success: false, }, + { + name: "invalid-maxIstioVersion", + configFile: ` +maxIstioVersion=v-2.31 +`, + success: false, + }, + { + name: "maxIstioVersion", + configFile: ` +maxIstioVersion=v1.24.0 +`, + expectedConfig: OperatorConfig{ + ImageDigests: map[string]IstioImageConfig{}, + MaximumIstioVersion: semver.MustParse("v1.24.0"), + }, + success: true, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) {