diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 90446db8ad..b6a68f587d 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -56,8 +56,11 @@ import ( const ( objectDeletionProtectionFlag = "object-deletion-protection" subobjectDeletionProtectionFlag = "subobject-deletion-protection" - objectDeletionProtectionDefault = true - subobjectDeletionProtectionDefault = true + objectDeletionProtectionDefault = false + subobjectDeletionProtectionDefault = false + + objectDeletionProtectionEnvVar = "UNSUPPORTED_OBJECT_DELETION_PROTECTION" + subobjectDeletionProtectionEnvVar = "UNSUPPORTED_SUBOBJECT_DELETION_PROTECTION" ) var ( @@ -237,10 +240,6 @@ func parseConfiguration() Config { "Enabling this will ensure there is only one active controller manager.") flag.StringVar(&config.LogLevel, "log-level", "info", "Log level. Available values: debug | info | warn | error | dpanic | panic | fatal") flag.StringVar(&config.LogEncoder, "log-encoder", "json", "Log encoder. Available values: json | console") - flag.BoolVar(&config.ObjectDeletionProtection, objectDeletionProtectionFlag, true, "Defines the operator will not delete Atlas resource "+ - "when a Custom Resource is deleted") - flag.BoolVar(&config.SubObjectDeletionProtection, subobjectDeletionProtectionFlag, true, "Defines that the operator will not overwrite "+ - "(and consequently delete) subresources that were not previously created by the operator") appVersion := flag.Bool("v", false, "prints application version") flag.Parse() @@ -264,7 +263,7 @@ func parseConfiguration() Config { config.Namespace = watchedNamespace } - configureDeletionProtectionFlags(&config) + configureDeletionProtection(&config) return config } @@ -319,45 +318,34 @@ func initCustomZapLogger(level, encoding string) (*zap.Logger, error) { return cfg.Build() } -func configureDeletionProtectionFlags(config *Config) { +func configureDeletionProtection(config *Config) { if config == nil { return } + config.ObjectDeletionProtection = objectDeletionProtectionDefault + config.SubObjectDeletionProtection = subobjectDeletionProtectionDefault - objectDeletionSet := false - subObjectDeletionSet := false - - flag.Visit(func(f *flag.Flag) { - if f.Name == objectDeletionProtectionFlag { - objectDeletionSet = true - } - - if f.Name == subobjectDeletionProtectionFlag { - subObjectDeletionSet = true - } - }) + // TODO: replace with the CLI flags at feature completion + enableDeletionProtectionFromEnvVars(config, version.Version) +} - if !objectDeletionSet { - objDeletion := strings.ToLower(os.Getenv("OBJECT_DELETION_PROTECTION")) - switch objDeletion { - case "true": - config.ObjectDeletionProtection = true - case "false": - config.ObjectDeletionProtection = false - default: - config.ObjectDeletionProtection = objectDeletionProtectionDefault +func enableDeletionProtectionFromEnvVars(config *Config, v string) { + if version.IsRelease(v) { + if isOn(os.Getenv(objectDeletionProtectionEnvVar)) || + isOn(os.Getenv(subobjectDeletionProtectionEnvVar)) { + log.Printf("Deletion Protection feature is not available yet in production releases") } + return } - if !subObjectDeletionSet { - objDeletion := strings.ToLower(os.Getenv("SUBOBJECT_DELETION_PROTECTION")) - switch objDeletion { - case "true": - config.SubObjectDeletionProtection = true - case "false": - config.SubObjectDeletionProtection = false - default: - config.SubObjectDeletionProtection = subobjectDeletionProtectionDefault - } + if isOn(os.Getenv(objectDeletionProtectionEnvVar)) { + config.ObjectDeletionProtection = true } + if isOn(os.Getenv(subobjectDeletionProtectionEnvVar)) { + config.SubObjectDeletionProtection = true + } +} + +func isOn(value string) bool { + return strings.ToLower(value) == "on" } diff --git a/cmd/manager/main_test.go b/cmd/manager/main_test.go new file mode 100644 index 0000000000..3f6d9ab2fa --- /dev/null +++ b/cmd/manager/main_test.go @@ -0,0 +1,89 @@ +package main + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/version" +) + +const ( + nonReleaseVersion = "1.8.0-30-g81233c6-dirty" + releaseVersion = "1.9.0-certified" +) + +func TestDeletionProtectionDisabledByDefault(t *testing.T) { + os.Unsetenv(objectDeletionProtectionEnvVar) + os.Unsetenv(subobjectDeletionProtectionEnvVar) + + cfg := Config{ + ObjectDeletionProtection: objectDeletionProtectionDefault, + SubObjectDeletionProtection: subobjectDeletionProtectionDefault, + } + enableDeletionProtectionFromEnvVars(&cfg, version.DefaultVersion) + + assert.Equal(t, false, cfg.ObjectDeletionProtection) + assert.Equal(t, false, cfg.SubObjectDeletionProtection) +} + +func TestDeletionProtectionIgnoredOnReleases(t *testing.T) { + version.Version = releaseVersion + os.Setenv(objectDeletionProtectionEnvVar, "On") + os.Setenv(subobjectDeletionProtectionEnvVar, "On") + + cfg := Config{ + ObjectDeletionProtection: objectDeletionProtectionDefault, + SubObjectDeletionProtection: subobjectDeletionProtectionDefault, + } + enableDeletionProtectionFromEnvVars(&cfg, releaseVersion) + + assert.Equal(t, false, cfg.ObjectDeletionProtection) + assert.Equal(t, false, cfg.SubObjectDeletionProtection) +} + +func TestDeletionProtectionEnabledAsEnvVars(t *testing.T) { + testCases := []struct { + title string + objDelProtect bool + subObjDelProtect bool + }{ + { + "both env vars set on non release version enables both protections", + true, + true, + }, + { + "obj env var set on non release version enables obj protection only", + true, + false, + }, + { + "subobj env var set on non release version enables subobj protection only", + false, + true, + }, + } + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + os.Unsetenv(objectDeletionProtectionEnvVar) + os.Unsetenv(subobjectDeletionProtectionEnvVar) + if tc.objDelProtect { + os.Setenv(objectDeletionProtectionEnvVar, "On") + } + if tc.subObjDelProtect { + os.Setenv(subobjectDeletionProtectionEnvVar, "On") + } + + cfg := Config{ + ObjectDeletionProtection: objectDeletionProtectionDefault, + SubObjectDeletionProtection: subobjectDeletionProtectionDefault, + } + enableDeletionProtectionFromEnvVars(&cfg, nonReleaseVersion) + + assert.Equal(t, tc.objDelProtect, cfg.ObjectDeletionProtection) + assert.Equal(t, tc.subObjDelProtect, cfg.SubObjectDeletionProtection) + }) + } +} diff --git a/helm-charts b/helm-charts index 35843312f5..0bf4037373 160000 --- a/helm-charts +++ b/helm-charts @@ -1 +1 @@ -Subproject commit 35843312f561b4413fb01424c674593270bcb942 +Subproject commit 0bf40373735d2d475246f8a1d9d2d13b45ac3f21 diff --git a/pkg/api/v1/zz_generated.deepcopy.go b/pkg/api/v1/zz_generated.deepcopy.go index e9496821a6..70057acb98 100644 --- a/pkg/api/v1/zz_generated.deepcopy.go +++ b/pkg/api/v1/zz_generated.deepcopy.go @@ -14,9 +14,10 @@ a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 package v1 import ( + "k8s.io/apimachinery/pkg/runtime" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/common" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/project" - "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/pkg/version/version.go b/pkg/version/version.go index c580c49981..da02f1a371 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,4 +1,16 @@ package version +import ( + "regexp" + "strings" +) + +const DefaultVersion = "unknown" + // Version set by the linker during link time. -var Version = "unknown" +var Version = DefaultVersion + +func IsRelease(v string) bool { + return v != DefaultVersion && + regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+[-certified]*$`).Match([]byte(strings.TrimSpace(v))) +} diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go new file mode 100644 index 0000000000..a5056b60e7 --- /dev/null +++ b/pkg/version/version_test.go @@ -0,0 +1,48 @@ +package version_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/version" +) + +func TestReleaseVersion(t *testing.T) { + testCases := []struct { + title string + version string + expected bool + }{ + { + "default is not release", + version.DefaultVersion, + false, + }, + { + "empty is not release", + "", + false, + }, + { + "dirty is not release", + "1.8.0-30-g81233c6-dirty", + false, + }, + { + "semver IS release", + "1.8.0", + true, + }, + { + "semver certified IS release", + "1.8.0-certified", + true, + }, + } + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + assert.Equal(t, tc.expected, version.IsRelease(tc.version)) + }) + } +} diff --git a/test/e2e/cli/helm/helm.go b/test/e2e/cli/helm/helm.go index 8a1d4cb53a..9c98d78c92 100644 --- a/test/e2e/cli/helm/helm.go +++ b/test/e2e/cli/helm/helm.go @@ -107,8 +107,6 @@ func InstallOperatorWideSubmodule(input model.UserInputs) { "atlas-operator-"+input.Project.GetProjectName(), config.AtlasOperatorHelmChartPath, "--set-string", fmt.Sprintf("atlasURI=%s", config.AtlasHost), - "--set", "objectDeletionProtection=false", - "--set", "subobjectDeletionProtection=false", "--set-string", fmt.Sprintf("image.repository=%s", repo), "--set-string", fmt.Sprintf("image.tag=%s", tag), "--namespace", input.Namespace, @@ -124,8 +122,6 @@ func InstallOperatorNamespacedFromLatestRelease(input model.UserInputs) { "mongodb/mongodb-atlas-operator", "--set", fmt.Sprintf("watchNamespaces={%s}", input.Namespace), "--set-string", fmt.Sprintf("atlasURI=%s", config.AtlasHost), - "--set", "objectDeletionProtection=false", - "--set", "subobjectDeletionProtection=false", "--namespace="+input.Namespace, "--create-namespace", ) @@ -145,8 +141,6 @@ func InstallOperatorNamespacedSubmodule(input model.UserInputs) { "--set-string", fmt.Sprintf("image.tag=%s", tag), "--set", fmt.Sprintf("watchNamespaces={%s}", input.Namespace), "--set", "mongodb-atlas-operator-crds.enabled=false", - "--set", "objectDeletionProtection=false", - "--set", "subobjectDeletionProtection=false", "--namespace="+input.Namespace, "--create-namespace", ) @@ -194,8 +188,6 @@ func UpgradeOperatorChart(input model.UserInputs) { "atlas-operator-"+input.Project.GetProjectName(), config.AtlasOperatorHelmChartPath, "--set-string", fmt.Sprintf("atlasURI=%s", config.AtlasHost), - "--set", "objectDeletionProtection=false", - "--set", "subobjectDeletionProtection=false", "--set-string", fmt.Sprintf("image.repository=%s", repo), "--set-string", fmt.Sprintf("image.tag=%s", tag), "-n", input.Namespace, diff --git a/test/e2e/helm_chart_test.go b/test/e2e/helm_chart_test.go index 5d82f70dd8..1635748d80 100644 --- a/test/e2e/helm_chart_test.go +++ b/test/e2e/helm_chart_test.go @@ -1,7 +1,6 @@ package e2e_test import ( - "context" "encoding/json" "fmt" "os" @@ -299,10 +298,7 @@ func deleteDeploymentAndOperator(data *model.TestDataProvider) { helm.Uninstall(data.Resources.Deployments[0].Spec.GetDeploymentName(), data.Resources.Namespace) Eventually( func(g Gomega) { - if atlasClient.IsProjectExists(g, data.Resources.ProjectID) { - _, err := atlasClient.Client.Projects.Delete(context.TODO(), data.Resources.ProjectID) - g.Expect(err).To(BeNil()) - } + atlasClient.IsProjectExists(g, data.Resources.ProjectID) }, "7m", "20s", ).Should(Succeed(), "Project and deployment should be deleted from Atlas")