From 37080526d9d10e0392accc3f802a0501323643f7 Mon Sep 17 00:00:00 2001 From: "jose.vazquez" Date: Tue, 16 Dec 2025 19:02:46 +0100 Subject: [PATCH 1/7] CLOUDP-358677: Add Flex e2e2 test Signed-off-by: jose.vazquez --- Makefile | 12 +- test/e2e2/e2e2_suite_test.go | 1 + test/e2e2/flexcluster_test.go | 278 ++++++++++++++++++ test/e2e2/flexsamples/samples.go | 32 ++ test/e2e2/flexsamples/test_group.yaml | 13 + .../e2e2/flexsamples/with_groupid_create.yaml | 17 ++ .../e2e2/flexsamples/with_groupid_update.yaml | 17 ++ .../flexsamples/with_groupref_create.yaml | 31 ++ .../flexsamples/with_groupref_update.yaml | 20 ++ test/helper/e2e2/kube/kube.go | 6 + 10 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 test/e2e2/flexcluster_test.go create mode 100644 test/e2e2/flexsamples/samples.go create mode 100644 test/e2e2/flexsamples/test_group.yaml create mode 100644 test/e2e2/flexsamples/with_groupid_create.yaml create mode 100644 test/e2e2/flexsamples/with_groupid_update.yaml create mode 100644 test/e2e2/flexsamples/with_groupref_create.yaml create mode 100644 test/e2e2/flexsamples/with_groupref_update.yaml diff --git a/Makefile b/Makefile index ed3763c100..9d38c35671 100644 --- a/Makefile +++ b/Makefile @@ -491,10 +491,13 @@ post-install-hook: x509-cert: ## Create X.509 cert at path tmp/x509/ (see docs/x509-user.md) go run scripts/create_x509.go -clean: ## Clean built binaries +.PHONY: clean-gen-crds +clean-gen-crds: ## Clean only generated CRD files + rm -f config/generated/crd/bases/crds.yaml + +clean: clean-gen-crds ## Clean built binaries rm -rf bin/* rm -rf config/manifests/bases/ - rm -f config/generated/crd/bases/crds.yaml rm -f config/crd/bases/*.yaml rm -f helm-charts/atlas-operator-crds/templates/*.yaml rm -f config/rbac/clusterwide/role.yaml @@ -607,7 +610,7 @@ clear-e2e-leftovers: ## Clear the e2e test leftovers quickly install-crds: manifests ## Install CRDs in Kubernetes kubectl apply -k config/crd ifdef EXPERIMENTAL - $(MAKE) clean gen-crds + $(MAKE) regen-crds kubectl apply -f config/generated/crd/bases/crds.yaml endif @@ -881,6 +884,9 @@ gen-crds: tools/openapi2crd/bin/openapi2crd --output $(realpath .)/config/generated/crd/bases/crds.yaml cp $(realpath .)/config/generated/crd/bases/crds.yaml $(realpath .)/internal/generated/crds/crds.yaml +.PHONY: regen-crds +regen-crds: clean-gen-crds gen-crds ## Clean and regenerate CRDs + gen-go-types: @echo "==> Generating Go models from CRDs..." $(CRD2GO) --input $(realpath .)/config/generated/crd/bases/crds.yaml \ diff --git a/test/e2e2/e2e2_suite_test.go b/test/e2e2/e2e2_suite_test.go index ad52a27c6c..ce62d0a4fc 100644 --- a/test/e2e2/e2e2_suite_test.go +++ b/test/e2e2/e2e2_suite_test.go @@ -75,6 +75,7 @@ func initTestLogging(t *testing.T) { ctrllog.SetLogger(logrLogger.WithName("test")) } +// nolint:unparam func runTestAKO(globalCreds, ns string, deletionprotection bool) operator.Operator { args := []string{ "--log-level=-9", diff --git a/test/e2e2/flexcluster_test.go b/test/e2e2/flexcluster_test.go new file mode 100644 index 0000000000..97c4d45c4b --- /dev/null +++ b/test/e2e2/flexcluster_test.go @@ -0,0 +1,278 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e2_test + +import ( + "context" + "os" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + nextapiv1 "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/version" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/state" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/e2e2/flexsamples" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/control" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e/utils" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/kube" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/operator" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/yml" +) + +const ( + FlexClusterCRDName = "flexclusters.atlas.generated.mongodb.com" + GroupCRDName = "groups.atlas.generated.mongodb.com" +) + +var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() { + var ctx context.Context + var kubeClient client.Client + var ako operator.Operator + var testNamespace *corev1.Namespace + var testGroup *nextapiv1.Group + var groupID string + var orgID string + + _ = BeforeAll(func() { + if !version.IsExperimental() { + Skip("FlexCluster is an experimental CRD and controller. Skipping test as experimental features are not enabled.") + } + + orgID = os.Getenv("MCLI_ORG_ID") + Expect(orgID).NotTo(BeEmpty(), "MCLI_ORG_ID environment variable must be set") + + deletionProtectionOff := false + ako = runTestAKO(DefaultGlobalCredentials, control.MustEnvVar("OPERATOR_NAMESPACE"), deletionProtectionOff) + ako.Start(GinkgoT()) + + ctx = context.Background() + testClient, err := kube.NewTestClient() + Expect(err).To(Succeed()) + kubeClient = testClient + Expect(kube.AssertCRDs(ctx, kubeClient, + &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: v1.ObjectMeta{Name: FlexClusterCRDName}, + }, + &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: v1.ObjectMeta{Name: GroupCRDName}, + }, + )).To(Succeed()) + + By("Create test Group", func() { + operatorNamespace := control.MustEnvVar("OPERATOR_NAMESPACE") + groupName := utils.RandomName("flexcluster-test-group") + // Replace placeholders in the Group YAML template + groupYAML := strings.ReplaceAll(string(flexsamples.TestGroup), "__GROUP_NAME__", groupName) + groupYAML = strings.ReplaceAll(groupYAML, "__OPERATOR_NAMESPACE__", operatorNamespace) + groupYAML = strings.ReplaceAll(groupYAML, "__CREDENTIALS_SECRET_NAME__", DefaultGlobalCredentials) + groupYAML = strings.ReplaceAll(groupYAML, "__ORG_ID__", orgID) + objs := yml.MustParseObjects(strings.NewReader(groupYAML)) + Expect(len(objs)).To(Equal(1)) + testGroup = objs[0].(*nextapiv1.Group) + Expect(kubeClient.Create(ctx, testGroup)).To(Succeed()) + }) + + By("Wait for Group to be Ready and get its ID", func() { + Eventually(func(g Gomega) bool { + g.Expect( + kubeClient.Get(ctx, client.ObjectKeyFromObject(testGroup), testGroup), + ).To(Succeed()) + if condition := meta.FindStatusCondition(testGroup.GetConditions(), "Ready"); condition != nil { + if condition.Status == metav1.ConditionTrue { + if testGroup.Status.V20250312 != nil && testGroup.Status.V20250312.Id != nil { + groupID = *testGroup.Status.V20250312.Id + return true + } + } + } + return false + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).To(BeTrue()) + Expect(groupID).NotTo(BeEmpty()) + }) + }) + + _ = AfterAll(func() { + if kubeClient != nil && testGroup != nil { + By("Clean up test Group", func() { + Expect(kubeClient.Delete(ctx, testGroup)).To(Succeed()) + Eventually(func(g Gomega) error { + err := kubeClient.Get(ctx, client.ObjectKeyFromObject(testGroup), testGroup) + return err + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).NotTo(Succeed()) + }) + } + if ako != nil { + ako.Stop(GinkgoT()) + } + }) + + _ = BeforeEach(func() { + testNamespace = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: utils.RandomName("flexcluster-ctlr-ns"), + }} + Expect(kubeClient.Create(ctx, testNamespace)).To(Succeed()) + Expect(ako.Running()).To(BeTrue(), "Operator must be running") + }) + + _ = AfterEach(func() { + if kubeClient == nil { + return + } + Expect( + kubeClient.Delete(ctx, testNamespace), + ).To(Succeed()) + Eventually(func(g Gomega) bool { + return kubeClient.Get(ctx, client.ObjectKeyFromObject(testNamespace), testNamespace) == nil + }).WithTimeout(time.Minute).WithPolling(time.Second).To(BeFalse()) + }) + + DescribeTable("FlexCluster CRUD lifecycle", + func(createYAML, updateYAML []byte, clusterName string) { + By("Copy credentials secret to test namespace", func() { + globalCredsKey := client.ObjectKey{ + Name: DefaultGlobalCredentials, + Namespace: control.MustEnvVar("OPERATOR_NAMESPACE"), + } + credentialsSecret, err := copySecretToNamespace(ctx, kubeClient, globalCredsKey, testNamespace.Name) + Expect(err).NotTo(HaveOccurred()) + Expect( + kubeClient.Patch(ctx, credentialsSecret, client.Apply, client.ForceOwnership, GinkGoFieldOwner), + ).To(Succeed()) + }) + + By("Create resources from YAML", func() { + // Replace placeholders with actual values + createYAMLStr := strings.ReplaceAll(string(createYAML), "__GROUP_ID__", groupID) + createYAMLStr = strings.ReplaceAll(createYAMLStr, "__ORG_ID__", orgID) + objs := yml.MustParseObjects(strings.NewReader(createYAMLStr)) + for _, obj := range objs { + objToApply := kube.WithRenamedNamespace(obj, testNamespace.Name) + Expect( + kubeClient.Patch(ctx, objToApply, client.Apply, client.ForceOwnership, GinkGoFieldOwner), + ).To(Succeed()) + } + }) + + By("Wait for Group to be Ready (if using groupRef)", func() { + createYAMLStr := strings.ReplaceAll(string(createYAML), "__GROUP_ID__", groupID) + createYAMLStr = strings.ReplaceAll(createYAMLStr, "__ORG_ID__", orgID) + objs := yml.MustParseObjects(strings.NewReader(createYAMLStr)) + for _, obj := range objs { + if group, ok := obj.(*nextapiv1.Group); ok { + groupInKube := nextapiv1.Group{ + ObjectMeta: metav1.ObjectMeta{Name: group.Name, Namespace: testNamespace.Name}, + } + Eventually(func(g Gomega) bool { + g.Expect( + kubeClient.Get(ctx, client.ObjectKeyFromObject(&groupInKube), &groupInKube), + ).To(Succeed()) + if condition := meta.FindStatusCondition(groupInKube.GetConditions(), "Ready"); condition != nil { + return condition.Status == metav1.ConditionTrue + } + return false + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).To(BeTrue()) + } + } + }) + + cluster := nextapiv1.FlexCluster{ + ObjectMeta: metav1.ObjectMeta{Name: clusterName, Namespace: testNamespace.Name}, + } + + By("Wait for FlexCluster to be Ready", func() { + Eventually(func(g Gomega) bool { + g.Expect( + kubeClient.Get(ctx, client.ObjectKeyFromObject(&cluster), &cluster), + ).To(Succeed()) + if condition := meta.FindStatusCondition(cluster.GetConditions(), "Ready"); condition != nil { + return condition.Status == metav1.ConditionTrue + } + return false + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).To(BeTrue()) + }) + + By("Verify cluster was created", func() { + Expect(cluster.Status.V20250312).NotTo(BeNil()) + Expect(cluster.Status.V20250312.Id).NotTo(BeNil()) + Expect(*cluster.Status.V20250312.Id).NotTo(BeEmpty()) + }) + + By("Update FlexCluster", func() { + if len(updateYAML) > 0 { + // Replace placeholders with actual values + updateYAMLStr := strings.ReplaceAll(string(updateYAML), "__GROUP_ID__", groupID) + updateYAMLStr = strings.ReplaceAll(updateYAMLStr, "__ORG_ID__", orgID) + updateObjs := yml.MustParseObjects(strings.NewReader(updateYAMLStr)) + for _, obj := range updateObjs { + objToPatch := kube.WithRenamedNamespace(obj, testNamespace.Name) + Expect( + kubeClient.Patch(ctx, objToPatch, client.Apply, client.ForceOwnership, GinkGoFieldOwner), + ).To(Succeed()) + } + } + }) + + By("Wait for FlexCluster to be Ready & updated", func() { + if len(updateYAML) > 0 { + Eventually(func(g Gomega) bool { + g.Expect( + kubeClient.Get(ctx, client.ObjectKeyFromObject(&cluster), &cluster), + ).To(Succeed()) + ready := false + if condition := meta.FindStatusCondition(cluster.GetConditions(), "Ready"); condition != nil { + ready = (condition.Status == metav1.ConditionTrue) + } + if ready { + if condition := meta.FindStatusCondition(cluster.GetConditions(), "State"); condition != nil { + return state.ResourceState(condition.Reason) == state.StateUpdated + } + } + return false + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).To(BeTrue()) + } + }) + + By("Delete FlexCluster", func() { + Expect(kubeClient.Delete(ctx, &cluster)).To(Succeed()) + }) + + By("Wait for FlexCluster to be deleted", func() { + Eventually(func(g Gomega) error { + err := kubeClient.Get(ctx, client.ObjectKeyFromObject(&cluster), &cluster) + return err + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).NotTo(Succeed()) + }) + }, + Entry("With direct groupId", + flexsamples.WithGroupIdCreate, + flexsamples.WithGroupIdUpdate, + "flexy", + ), + Entry("With groupRef", + flexsamples.WithGroupRefCreate, + flexsamples.WithGroupRefUpdate, + "flexy", + ), + ) +}) diff --git a/test/e2e2/flexsamples/samples.go b/test/e2e2/flexsamples/samples.go new file mode 100644 index 0000000000..6c5edd55db --- /dev/null +++ b/test/e2e2/flexsamples/samples.go @@ -0,0 +1,32 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flexsamples + +import _ "embed" + +//go:embed with_groupid_create.yaml +var WithGroupIdCreate []byte + +//go:embed with_groupid_update.yaml +var WithGroupIdUpdate []byte + +//go:embed with_groupref_create.yaml +var WithGroupRefCreate []byte + +//go:embed with_groupref_update.yaml +var WithGroupRefUpdate []byte + +//go:embed test_group.yaml +var TestGroup []byte diff --git a/test/e2e2/flexsamples/test_group.yaml b/test/e2e2/flexsamples/test_group.yaml new file mode 100644 index 0000000000..df930bd6e3 --- /dev/null +++ b/test/e2e2/flexsamples/test_group.yaml @@ -0,0 +1,13 @@ +apiVersion: atlas.generated.mongodb.com/v1 +kind: Group +metadata: + name: __GROUP_NAME__ + namespace: __OPERATOR_NAMESPACE__ +spec: + connectionSecretRef: + name: __CREDENTIALS_SECRET_NAME__ + v20250312: + entry: + orgId: __ORG_ID__ + name: __GROUP_NAME__ + diff --git a/test/e2e2/flexsamples/with_groupid_create.yaml b/test/e2e2/flexsamples/with_groupid_create.yaml new file mode 100644 index 0000000000..c6ce5eeb80 --- /dev/null +++ b/test/e2e2/flexsamples/with_groupid_create.yaml @@ -0,0 +1,17 @@ +apiVersion: atlas.generated.mongodb.com/v1 +kind: FlexCluster +metadata: + name: flexy + namespace: mongodb-atlas-system +spec: + connectionSecretRef: + name: mongodb-atlas-operator-api-key + v20250312: + groupId: __GROUP_ID__ + entry: + name: flexy + terminationProtectionEnabled: true + providerSettings: + backingProviderName: GCP + regionName: CENTRAL_US + diff --git a/test/e2e2/flexsamples/with_groupid_update.yaml b/test/e2e2/flexsamples/with_groupid_update.yaml new file mode 100644 index 0000000000..b1b8afebc9 --- /dev/null +++ b/test/e2e2/flexsamples/with_groupid_update.yaml @@ -0,0 +1,17 @@ +apiVersion: atlas.generated.mongodb.com/v1 +kind: FlexCluster +metadata: + name: flexy + namespace: mongodb-atlas-system +spec: + connectionSecretRef: + name: mongodb-atlas-operator-api-key + v20250312: + groupId: __GROUP_ID__ + entry: + name: flexy + terminationProtectionEnabled: false + providerSettings: + backingProviderName: GCP + regionName: CENTRAL_US + diff --git a/test/e2e2/flexsamples/with_groupref_create.yaml b/test/e2e2/flexsamples/with_groupref_create.yaml new file mode 100644 index 0000000000..0fda861ef2 --- /dev/null +++ b/test/e2e2/flexsamples/with_groupref_create.yaml @@ -0,0 +1,31 @@ +apiVersion: atlas.generated.mongodb.com/v1 +kind: Group +metadata: + name: test-flexy + namespace: mongodb-atlas-system +spec: + connectionSecretRef: + name: mongodb-atlas-operator-api-key + v20250312: + entry: + orgId: __ORG_ID__ + name: test-flexy +--- +apiVersion: atlas.generated.mongodb.com/v1 +kind: FlexCluster +metadata: + name: flexy + namespace: mongodb-atlas-system + annotations: + some-tag: tag +spec: + v20250312: + groupRef: + name: test-flexy + entry: + name: flexy + terminationProtectionEnabled: true + providerSettings: + backingProviderName: GCP + regionName: CENTRAL_US + diff --git a/test/e2e2/flexsamples/with_groupref_update.yaml b/test/e2e2/flexsamples/with_groupref_update.yaml new file mode 100644 index 0000000000..71faa12233 --- /dev/null +++ b/test/e2e2/flexsamples/with_groupref_update.yaml @@ -0,0 +1,20 @@ +apiVersion: atlas.generated.mongodb.com/v1 +kind: FlexCluster +metadata: + name: flexy + namespace: mongodb-atlas-system + annotations: + some-tag: tag +spec: + connectionSecretRef: + name: mongodb-atlas-operator-api-key + v20250312: + groupRef: + name: test-flexy + entry: + name: flexy + terminationProtectionEnabled: false + providerSettings: + backingProviderName: GCP + regionName: CENTRAL_US + diff --git a/test/helper/e2e2/kube/kube.go b/test/helper/e2e2/kube/kube.go index d5041f8a93..442c39f49c 100644 --- a/test/helper/e2e2/kube/kube.go +++ b/test/helper/e2e2/kube/kube.go @@ -29,6 +29,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1" + generatedv1 "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/version" ) const ( @@ -59,6 +61,10 @@ func NewTestClient() (client.Client, error) { utilruntime.Must(apiextensionsv1.AddToScheme(testScheme)) utilruntime.Must(akov2.AddToScheme(testScheme)) utilruntime.Must(appsv1.AddToScheme(testScheme)) + // Add experimental nextapi types (e.g., FlexCluster, Group) only when experimental features are enabled + if version.IsExperimental() { + utilruntime.Must(generatedv1.AddToScheme(testScheme)) + } return getKubeClient(testScheme) } From 65ec82d6c98761ce6cc91cb3c3173eaf912f80bc Mon Sep 17 00:00:00 2001 From: "jose.vazquez" Date: Wed, 17 Dec 2025 16:09:45 +0100 Subject: [PATCH 2/7] Fix the new e2e2 test Signed-off-by: jose.vazquez --- test/e2e2/flexcluster_test.go | 248 +++++++++++------- .../e2e2/flexsamples/with_groupid_create.yaml | 2 - .../e2e2/flexsamples/with_groupid_update.yaml | 2 - .../flexsamples/with_groupref_create.yaml | 9 +- .../flexsamples/with_groupref_update.yaml | 4 +- test/helper/e2e2/kube/kube.go | 12 + 6 files changed, 162 insertions(+), 115 deletions(-) diff --git a/test/e2e2/flexcluster_test.go b/test/e2e2/flexcluster_test.go index 97c4d45c4b..558f431e6e 100644 --- a/test/e2e2/flexcluster_test.go +++ b/test/e2e2/flexcluster_test.go @@ -23,10 +23,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" nextapiv1 "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1" @@ -45,14 +43,25 @@ const ( GroupCRDName = "groups.atlas.generated.mongodb.com" ) +// yamlPlaceholders holds all placeholder values for YAML template replacement. +type yamlPlaceholders struct { + GroupID string + OrgID string + GroupName string + OperatorNamespace string + CredentialsSecretName string +} + var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() { var ctx context.Context var kubeClient client.Client var ako operator.Operator var testNamespace *corev1.Namespace + var sharedGroupNamespace *corev1.Namespace var testGroup *nextapiv1.Group var groupID string var orgID string + var sharedPlaceholders yamlPlaceholders _ = BeforeAll(func() { if !version.IsExperimental() { @@ -70,23 +79,27 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() testClient, err := kube.NewTestClient() Expect(err).To(Succeed()) kubeClient = testClient - Expect(kube.AssertCRDs(ctx, kubeClient, - &apiextensionsv1.CustomResourceDefinition{ - ObjectMeta: v1.ObjectMeta{Name: FlexClusterCRDName}, - }, - &apiextensionsv1.CustomResourceDefinition{ - ObjectMeta: v1.ObjectMeta{Name: GroupCRDName}, - }, - )).To(Succeed()) + Expect(kube.AssertCRDNames(ctx, kubeClient, FlexClusterCRDName, GroupCRDName)).To(Succeed()) + + By("Create namespace and credentials for shared test Group", func() { + sharedGroupNamespace = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: utils.RandomName("flex-shared-grp-ns"), + }} + Expect(kubeClient.Create(ctx, sharedGroupNamespace)).To(Succeed()) + copyCredentialsToNamespace(ctx, kubeClient, sharedGroupNamespace.Name) + }) By("Create test Group", func() { - operatorNamespace := control.MustEnvVar("OPERATOR_NAMESPACE") groupName := utils.RandomName("flexcluster-test-group") + // Set up shared placeholders for Group YAML template + sharedPlaceholders = yamlPlaceholders{ + GroupName: groupName, + OperatorNamespace: sharedGroupNamespace.Name, + CredentialsSecretName: DefaultGlobalCredentials, + OrgID: orgID, + } // Replace placeholders in the Group YAML template - groupYAML := strings.ReplaceAll(string(flexsamples.TestGroup), "__GROUP_NAME__", groupName) - groupYAML = strings.ReplaceAll(groupYAML, "__OPERATOR_NAMESPACE__", operatorNamespace) - groupYAML = strings.ReplaceAll(groupYAML, "__CREDENTIALS_SECRET_NAME__", DefaultGlobalCredentials) - groupYAML = strings.ReplaceAll(groupYAML, "__ORG_ID__", orgID) + groupYAML := replaceYAMLPlaceholders(string(flexsamples.TestGroup), sharedPlaceholders) objs := yml.MustParseObjects(strings.NewReader(groupYAML)) Expect(len(objs)).To(Equal(1)) testGroup = objs[0].(*nextapiv1.Group) @@ -94,21 +107,13 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() }) By("Wait for Group to be Ready and get its ID", func() { - Eventually(func(g Gomega) bool { - g.Expect( - kubeClient.Get(ctx, client.ObjectKeyFromObject(testGroup), testGroup), - ).To(Succeed()) - if condition := meta.FindStatusCondition(testGroup.GetConditions(), "Ready"); condition != nil { - if condition.Status == metav1.ConditionTrue { - if testGroup.Status.V20250312 != nil && testGroup.Status.V20250312.Id != nil { - groupID = *testGroup.Status.V20250312.Id - return true - } - } - } - return false - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).To(BeTrue()) + waitForResourceReady(ctx, kubeClient, testGroup) + Expect(testGroup.Status.V20250312).NotTo(BeNil()) + Expect(testGroup.Status.V20250312.Id).NotTo(BeNil()) + groupID = *testGroup.Status.V20250312.Id Expect(groupID).NotTo(BeEmpty()) + // Update shared placeholders with groupID now that it's available + sharedPlaceholders.GroupID = groupID }) }) @@ -122,6 +127,14 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).NotTo(Succeed()) }) } + if kubeClient != nil && sharedGroupNamespace != nil { + By("Clean up shared group namespace", func() { + Expect(kubeClient.Delete(ctx, sharedGroupNamespace)).To(Succeed()) + Eventually(func(g Gomega) bool { + return kubeClient.Get(ctx, client.ObjectKeyFromObject(sharedGroupNamespace), sharedGroupNamespace) == nil + }).WithTimeout(time.Minute).WithPolling(time.Second).To(BeFalse()) + }) + } if ako != nil { ako.Stop(GinkgoT()) } @@ -149,49 +162,33 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() DescribeTable("FlexCluster CRUD lifecycle", func(createYAML, updateYAML []byte, clusterName string) { + // Generate randomized group name for this test run (cluster names are unique per group) + groupName := utils.RandomName("flex-grp") + + // Set up placeholders for this test case (reuse shared values, override groupName) + testPlaceholders := sharedPlaceholders + testPlaceholders.GroupName = groupName + + // Track created objects for cleanup + var createdObjects []client.Object + By("Copy credentials secret to test namespace", func() { - globalCredsKey := client.ObjectKey{ - Name: DefaultGlobalCredentials, - Namespace: control.MustEnvVar("OPERATOR_NAMESPACE"), - } - credentialsSecret, err := copySecretToNamespace(ctx, kubeClient, globalCredsKey, testNamespace.Name) - Expect(err).NotTo(HaveOccurred()) - Expect( - kubeClient.Patch(ctx, credentialsSecret, client.Apply, client.ForceOwnership, GinkGoFieldOwner), - ).To(Succeed()) + copyCredentialsToNamespace(ctx, kubeClient, testNamespace.Name) }) By("Create resources from YAML", func() { - // Replace placeholders with actual values - createYAMLStr := strings.ReplaceAll(string(createYAML), "__GROUP_ID__", groupID) - createYAMLStr = strings.ReplaceAll(createYAMLStr, "__ORG_ID__", orgID) - objs := yml.MustParseObjects(strings.NewReader(createYAMLStr)) - for _, obj := range objs { - objToApply := kube.WithRenamedNamespace(obj, testNamespace.Name) - Expect( - kubeClient.Patch(ctx, objToApply, client.Apply, client.ForceOwnership, GinkGoFieldOwner), - ).To(Succeed()) - } + objs := applyYAMLToNamespace(ctx, kubeClient, createYAML, testPlaceholders, testNamespace.Name) + createdObjects = append(createdObjects, objs...) }) By("Wait for Group to be Ready (if using groupRef)", func() { - createYAMLStr := strings.ReplaceAll(string(createYAML), "__GROUP_ID__", groupID) - createYAMLStr = strings.ReplaceAll(createYAMLStr, "__ORG_ID__", orgID) + createYAMLStr := replaceYAMLPlaceholders(string(createYAML), testPlaceholders) objs := yml.MustParseObjects(strings.NewReader(createYAMLStr)) for _, obj := range objs { if group, ok := obj.(*nextapiv1.Group); ok { - groupInKube := nextapiv1.Group{ + waitForResourceReady(ctx, kubeClient, &nextapiv1.Group{ ObjectMeta: metav1.ObjectMeta{Name: group.Name, Namespace: testNamespace.Name}, - } - Eventually(func(g Gomega) bool { - g.Expect( - kubeClient.Get(ctx, client.ObjectKeyFromObject(&groupInKube), &groupInKube), - ).To(Succeed()) - if condition := meta.FindStatusCondition(groupInKube.GetConditions(), "Ready"); condition != nil { - return condition.Status == metav1.ConditionTrue - } - return false - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).To(BeTrue()) + }) } } }) @@ -201,15 +198,7 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() } By("Wait for FlexCluster to be Ready", func() { - Eventually(func(g Gomega) bool { - g.Expect( - kubeClient.Get(ctx, client.ObjectKeyFromObject(&cluster), &cluster), - ).To(Succeed()) - if condition := meta.FindStatusCondition(cluster.GetConditions(), "Ready"); condition != nil { - return condition.Status == metav1.ConditionTrue - } - return false - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).To(BeTrue()) + waitForResourceReady(ctx, kubeClient, &cluster) }) By("Verify cluster was created", func() { @@ -220,48 +209,26 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() By("Update FlexCluster", func() { if len(updateYAML) > 0 { - // Replace placeholders with actual values - updateYAMLStr := strings.ReplaceAll(string(updateYAML), "__GROUP_ID__", groupID) - updateYAMLStr = strings.ReplaceAll(updateYAMLStr, "__ORG_ID__", orgID) - updateObjs := yml.MustParseObjects(strings.NewReader(updateYAMLStr)) - for _, obj := range updateObjs { - objToPatch := kube.WithRenamedNamespace(obj, testNamespace.Name) - Expect( - kubeClient.Patch(ctx, objToPatch, client.Apply, client.ForceOwnership, GinkGoFieldOwner), - ).To(Succeed()) - } + applyYAMLToNamespace(ctx, kubeClient, updateYAML, testPlaceholders, testNamespace.Name) } }) By("Wait for FlexCluster to be Ready & updated", func() { if len(updateYAML) > 0 { - Eventually(func(g Gomega) bool { - g.Expect( - kubeClient.Get(ctx, client.ObjectKeyFromObject(&cluster), &cluster), - ).To(Succeed()) - ready := false - if condition := meta.FindStatusCondition(cluster.GetConditions(), "Ready"); condition != nil { - ready = (condition.Status == metav1.ConditionTrue) - } - if ready { - if condition := meta.FindStatusCondition(cluster.GetConditions(), "State"); condition != nil { - return state.ResourceState(condition.Reason) == state.StateUpdated - } - } - return false - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).To(BeTrue()) + waitForResourceUpdated(ctx, kubeClient, &cluster) } }) - By("Delete FlexCluster", func() { - Expect(kubeClient.Delete(ctx, &cluster)).To(Succeed()) + By("Delete all created resources", func() { + for _, obj := range createdObjects { + _ = kubeClient.Delete(ctx, obj) + } }) - By("Wait for FlexCluster to be deleted", func() { - Eventually(func(g Gomega) error { - err := kubeClient.Get(ctx, client.ObjectKeyFromObject(&cluster), &cluster) - return err - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).NotTo(Succeed()) + By("Wait for all resources to be deleted", func() { + for _, obj := range createdObjects { + waitForResourceDeleted(ctx, kubeClient, obj) + } }) }, Entry("With direct groupId", @@ -276,3 +243,80 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() ), ) }) + +// replaceYAMLPlaceholders replaces placeholders in YAML templates with actual values from the struct. +func replaceYAMLPlaceholders(yaml string, p yamlPlaceholders) string { + result := yaml + result = strings.ReplaceAll(result, "__GROUP_ID__", p.GroupID) + result = strings.ReplaceAll(result, "__ORG_ID__", p.OrgID) + result = strings.ReplaceAll(result, "__GROUP_NAME__", p.GroupName) + result = strings.ReplaceAll(result, "__OPERATOR_NAMESPACE__", p.OperatorNamespace) + result = strings.ReplaceAll(result, "__CREDENTIALS_SECRET_NAME__", p.CredentialsSecretName) + return result +} + +// copyCredentialsToNamespace copies the default global credentials secret to the specified namespace. +func copyCredentialsToNamespace(ctx context.Context, kubeClient client.Client, namespace string) { + globalCredsKey := client.ObjectKey{ + Name: DefaultGlobalCredentials, + Namespace: control.MustEnvVar("OPERATOR_NAMESPACE"), + } + credentialsSecret, err := copySecretToNamespace(ctx, kubeClient, globalCredsKey, namespace) + Expect(err).NotTo(HaveOccurred()) + Expect( + kubeClient.Patch(ctx, credentialsSecret, client.Apply, client.ForceOwnership, GinkGoFieldOwner), + ).To(Succeed()) +} + +// applyYAMLToNamespace applies YAML objects to a namespace after replacing placeholders. +// Returns the list of applied objects. +func applyYAMLToNamespace(ctx context.Context, kubeClient client.Client, yaml []byte, placeholders yamlPlaceholders, namespace string) []client.Object { + yamlStr := replaceYAMLPlaceholders(string(yaml), placeholders) + objs := yml.MustParseObjects(strings.NewReader(yamlStr)) + for _, obj := range objs { + obj.SetNamespace(namespace) + Expect( + kubeClient.Patch(ctx, obj, client.Apply, client.ForceOwnership, GinkGoFieldOwner), + ).To(Succeed()) + } + return objs +} + +// waitForResourceReady waits for a resource to have Ready condition set to True. +func waitForResourceReady(ctx context.Context, kubeClient client.Client, obj kube.ObjectWithStatus) { + Eventually(func(g Gomega) bool { + g.Expect( + kubeClient.Get(ctx, client.ObjectKeyFromObject(obj), obj), + ).To(Succeed()) + if condition := meta.FindStatusCondition(obj.GetConditions(), "Ready"); condition != nil { + return condition.Status == metav1.ConditionTrue + } + return false + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).To(BeTrue()) +} + +// waitForResourceUpdated waits for a resource to be Ready and in Updated state. +func waitForResourceUpdated(ctx context.Context, kubeClient client.Client, obj kube.ObjectWithStatus) { + Eventually(func(g Gomega) bool { + g.Expect( + kubeClient.Get(ctx, client.ObjectKeyFromObject(obj), obj), + ).To(Succeed()) + ready := false + if condition := meta.FindStatusCondition(obj.GetConditions(), "Ready"); condition != nil { + ready = (condition.Status == metav1.ConditionTrue) + } + if ready { + if condition := meta.FindStatusCondition(obj.GetConditions(), "State"); condition != nil { + return state.ResourceState(condition.Reason) == state.StateUpdated + } + } + return false + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).To(BeTrue()) +} + +// waitForResourceDeleted waits for a resource to be deleted from the cluster. +func waitForResourceDeleted(ctx context.Context, kubeClient client.Client, obj client.Object) { + Eventually(func(g Gomega) error { + return kubeClient.Get(ctx, client.ObjectKeyFromObject(obj), obj) + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).ShouldNot(Succeed()) +} diff --git a/test/e2e2/flexsamples/with_groupid_create.yaml b/test/e2e2/flexsamples/with_groupid_create.yaml index c6ce5eeb80..d9fff213b3 100644 --- a/test/e2e2/flexsamples/with_groupid_create.yaml +++ b/test/e2e2/flexsamples/with_groupid_create.yaml @@ -2,7 +2,6 @@ apiVersion: atlas.generated.mongodb.com/v1 kind: FlexCluster metadata: name: flexy - namespace: mongodb-atlas-system spec: connectionSecretRef: name: mongodb-atlas-operator-api-key @@ -14,4 +13,3 @@ spec: providerSettings: backingProviderName: GCP regionName: CENTRAL_US - diff --git a/test/e2e2/flexsamples/with_groupid_update.yaml b/test/e2e2/flexsamples/with_groupid_update.yaml index b1b8afebc9..66f1902dc3 100644 --- a/test/e2e2/flexsamples/with_groupid_update.yaml +++ b/test/e2e2/flexsamples/with_groupid_update.yaml @@ -2,7 +2,6 @@ apiVersion: atlas.generated.mongodb.com/v1 kind: FlexCluster metadata: name: flexy - namespace: mongodb-atlas-system spec: connectionSecretRef: name: mongodb-atlas-operator-api-key @@ -14,4 +13,3 @@ spec: providerSettings: backingProviderName: GCP regionName: CENTRAL_US - diff --git a/test/e2e2/flexsamples/with_groupref_create.yaml b/test/e2e2/flexsamples/with_groupref_create.yaml index 0fda861ef2..31d8141268 100644 --- a/test/e2e2/flexsamples/with_groupref_create.yaml +++ b/test/e2e2/flexsamples/with_groupref_create.yaml @@ -1,31 +1,28 @@ apiVersion: atlas.generated.mongodb.com/v1 kind: Group metadata: - name: test-flexy - namespace: mongodb-atlas-system + name: __GROUP_NAME__ spec: connectionSecretRef: name: mongodb-atlas-operator-api-key v20250312: entry: orgId: __ORG_ID__ - name: test-flexy + name: __GROUP_NAME__ --- apiVersion: atlas.generated.mongodb.com/v1 kind: FlexCluster metadata: name: flexy - namespace: mongodb-atlas-system annotations: some-tag: tag spec: v20250312: groupRef: - name: test-flexy + name: __GROUP_NAME__ entry: name: flexy terminationProtectionEnabled: true providerSettings: backingProviderName: GCP regionName: CENTRAL_US - diff --git a/test/e2e2/flexsamples/with_groupref_update.yaml b/test/e2e2/flexsamples/with_groupref_update.yaml index 71faa12233..d6269beb0d 100644 --- a/test/e2e2/flexsamples/with_groupref_update.yaml +++ b/test/e2e2/flexsamples/with_groupref_update.yaml @@ -2,7 +2,6 @@ apiVersion: atlas.generated.mongodb.com/v1 kind: FlexCluster metadata: name: flexy - namespace: mongodb-atlas-system annotations: some-tag: tag spec: @@ -10,11 +9,10 @@ spec: name: mongodb-atlas-operator-api-key v20250312: groupRef: - name: test-flexy + name: __GROUP_NAME__ entry: name: flexy terminationProtectionEnabled: false providerSettings: backingProviderName: GCP regionName: CENTRAL_US - diff --git a/test/helper/e2e2/kube/kube.go b/test/helper/e2e2/kube/kube.go index 442c39f49c..354bae505b 100644 --- a/test/helper/e2e2/kube/kube.go +++ b/test/helper/e2e2/kube/kube.go @@ -42,6 +42,18 @@ type ObjectWithStatus interface { GetConditions() []metav1.Condition } +// AssertCRDNames check that the given names are CRDs installed in the accesible cluster +func AssertCRDNames(ctx context.Context, kubeClient client.Client, crdNames ... string) error { + crds := make([]*apiextensionsv1.CustomResourceDefinition, 0, len(crdNames)) + for _, crdName := range crdNames { + crd := &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: crdName}, + } + crds = append(crds, crd) + } + return AssertCRDs(ctx, kubeClient, crds...) +} + // AssertCRDs check that the given CRDs are installed in the accesible cluster func AssertCRDs(ctx context.Context, kubeClient client.Client, crds ...*apiextensionsv1.CustomResourceDefinition) error { for _, targetCRD := range crds { From 600d201ab2e7c104525eb2a099e9e5ad05fedc74 Mon Sep 17 00:00:00 2001 From: "jose.vazquez" Date: Thu, 18 Dec 2025 10:57:06 +0100 Subject: [PATCH 3/7] Refactor and simplify e2e2 Signed-off-by: jose.vazquez --- test/e2e2/flexcluster_test.go | 148 +++++----------------- test/helper/e2e2/kube/kube.go | 2 +- test/helper/e2e2/resources/resources.go | 139 ++++++++++++++++++++ test/helper/e2e2/testparams/testparams.go | 83 ++++++++++++ 4 files changed, 258 insertions(+), 114 deletions(-) create mode 100644 test/helper/e2e2/resources/resources.go create mode 100644 test/helper/e2e2/testparams/testparams.go diff --git a/test/e2e2/flexcluster_test.go b/test/e2e2/flexcluster_test.go index 558f431e6e..40bc8e996f 100644 --- a/test/e2e2/flexcluster_test.go +++ b/test/e2e2/flexcluster_test.go @@ -23,18 +23,18 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" nextapiv1 "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/version" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/state" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/e2e2/flexsamples" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/control" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e/utils" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/kube" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/operator" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/resources" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/testparams" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/yml" ) @@ -43,15 +43,6 @@ const ( GroupCRDName = "groups.atlas.generated.mongodb.com" ) -// yamlPlaceholders holds all placeholder values for YAML template replacement. -type yamlPlaceholders struct { - GroupID string - OrgID string - GroupName string - OperatorNamespace string - CredentialsSecretName string -} - var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() { var ctx context.Context var kubeClient client.Client @@ -61,7 +52,7 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() var testGroup *nextapiv1.Group var groupID string var orgID string - var sharedPlaceholders yamlPlaceholders + var sharedTestParams *testparams.TestParams _ = BeforeAll(func() { if !version.IsExperimental() { @@ -86,20 +77,16 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() Name: utils.RandomName("flex-shared-grp-ns"), }} Expect(kubeClient.Create(ctx, sharedGroupNamespace)).To(Succeed()) - copyCredentialsToNamespace(ctx, kubeClient, sharedGroupNamespace.Name) + Expect(resources.CopyCredentialsToNamespace(ctx, kubeClient, DefaultGlobalCredentials, control.MustEnvVar("OPERATOR_NAMESPACE"), sharedGroupNamespace.Name, GinkGoFieldOwner)).To(Succeed()) }) By("Create test Group", func() { groupName := utils.RandomName("flexcluster-test-group") - // Set up shared placeholders for Group YAML template - sharedPlaceholders = yamlPlaceholders{ - GroupName: groupName, - OperatorNamespace: sharedGroupNamespace.Name, - CredentialsSecretName: DefaultGlobalCredentials, - OrgID: orgID, - } + // Set up shared test params for Group YAML template + sharedTestParams = testparams.New(orgID, sharedGroupNamespace.Name, DefaultGlobalCredentials). + WithGroupName(groupName) // Replace placeholders in the Group YAML template - groupYAML := replaceYAMLPlaceholders(string(flexsamples.TestGroup), sharedPlaceholders) + groupYAML := sharedTestParams.ReplaceYAML(string(flexsamples.TestGroup)) objs := yml.MustParseObjects(strings.NewReader(groupYAML)) Expect(len(objs)).To(Equal(1)) testGroup = objs[0].(*nextapiv1.Group) @@ -107,13 +94,15 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() }) By("Wait for Group to be Ready and get its ID", func() { - waitForResourceReady(ctx, kubeClient, testGroup) + Eventually(func(g Gomega) { + g.Expect(resources.CheckResourceReady(ctx, kubeClient, testGroup)).To(Succeed()) + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) Expect(testGroup.Status.V20250312).NotTo(BeNil()) Expect(testGroup.Status.V20250312.Id).NotTo(BeNil()) groupID = *testGroup.Status.V20250312.Id Expect(groupID).NotTo(BeEmpty()) - // Update shared placeholders with groupID now that it's available - sharedPlaceholders.GroupID = groupID + // Update shared test params with groupID now that it's available + sharedTestParams = sharedTestParams.WithGroupID(groupID) }) }) @@ -165,30 +154,33 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() // Generate randomized group name for this test run (cluster names are unique per group) groupName := utils.RandomName("flex-grp") - // Set up placeholders for this test case (reuse shared values, override groupName) - testPlaceholders := sharedPlaceholders - testPlaceholders.GroupName = groupName + // Set up test params for this test case (reuse shared values, override groupName) + testParams := sharedTestParams.WithGroupName(groupName) // Track created objects for cleanup var createdObjects []client.Object By("Copy credentials secret to test namespace", func() { - copyCredentialsToNamespace(ctx, kubeClient, testNamespace.Name) + Expect(resources.CopyCredentialsToNamespace(ctx, kubeClient, DefaultGlobalCredentials, control.MustEnvVar("OPERATOR_NAMESPACE"), testNamespace.Name, GinkGoFieldOwner)).To(Succeed()) }) By("Create resources from YAML", func() { - objs := applyYAMLToNamespace(ctx, kubeClient, createYAML, testPlaceholders, testNamespace.Name) + objs, err := resources.ApplyYAMLToNamespace(ctx, kubeClient, createYAML, testParams, testNamespace.Name, GinkGoFieldOwner) + Expect(err).NotTo(HaveOccurred()) createdObjects = append(createdObjects, objs...) }) By("Wait for Group to be Ready (if using groupRef)", func() { - createYAMLStr := replaceYAMLPlaceholders(string(createYAML), testPlaceholders) + createYAMLStr := testParams.ReplaceYAML(string(createYAML)) objs := yml.MustParseObjects(strings.NewReader(createYAMLStr)) for _, obj := range objs { if group, ok := obj.(*nextapiv1.Group); ok { - waitForResourceReady(ctx, kubeClient, &nextapiv1.Group{ + groupObj := &nextapiv1.Group{ ObjectMeta: metav1.ObjectMeta{Name: group.Name, Namespace: testNamespace.Name}, - }) + } + Eventually(func(g Gomega) { + g.Expect(resources.CheckResourceReady(ctx, kubeClient, groupObj)).To(Succeed()) + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) } } }) @@ -198,7 +190,9 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() } By("Wait for FlexCluster to be Ready", func() { - waitForResourceReady(ctx, kubeClient, &cluster) + Eventually(func(g Gomega) { + g.Expect(resources.CheckResourceReady(ctx, kubeClient, &cluster)).To(Succeed()) + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) }) By("Verify cluster was created", func() { @@ -209,13 +203,16 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() By("Update FlexCluster", func() { if len(updateYAML) > 0 { - applyYAMLToNamespace(ctx, kubeClient, updateYAML, testPlaceholders, testNamespace.Name) + _, err := resources.ApplyYAMLToNamespace(ctx, kubeClient, updateYAML, testParams, testNamespace.Name, GinkGoFieldOwner) + Expect(err).NotTo(HaveOccurred()) } }) By("Wait for FlexCluster to be Ready & updated", func() { if len(updateYAML) > 0 { - waitForResourceUpdated(ctx, kubeClient, &cluster) + Eventually(func(g Gomega) { + g.Expect(resources.CheckResourceUpdated(ctx, kubeClient, &cluster)).To(Succeed()) + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) } }) @@ -227,7 +224,9 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() By("Wait for all resources to be deleted", func() { for _, obj := range createdObjects { - waitForResourceDeleted(ctx, kubeClient, obj) + Eventually(func(g Gomega) { + g.Expect(resources.CheckResourceDeleted(ctx, kubeClient, obj)).To(Succeed()) + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) } }) }, @@ -243,80 +242,3 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() ), ) }) - -// replaceYAMLPlaceholders replaces placeholders in YAML templates with actual values from the struct. -func replaceYAMLPlaceholders(yaml string, p yamlPlaceholders) string { - result := yaml - result = strings.ReplaceAll(result, "__GROUP_ID__", p.GroupID) - result = strings.ReplaceAll(result, "__ORG_ID__", p.OrgID) - result = strings.ReplaceAll(result, "__GROUP_NAME__", p.GroupName) - result = strings.ReplaceAll(result, "__OPERATOR_NAMESPACE__", p.OperatorNamespace) - result = strings.ReplaceAll(result, "__CREDENTIALS_SECRET_NAME__", p.CredentialsSecretName) - return result -} - -// copyCredentialsToNamespace copies the default global credentials secret to the specified namespace. -func copyCredentialsToNamespace(ctx context.Context, kubeClient client.Client, namespace string) { - globalCredsKey := client.ObjectKey{ - Name: DefaultGlobalCredentials, - Namespace: control.MustEnvVar("OPERATOR_NAMESPACE"), - } - credentialsSecret, err := copySecretToNamespace(ctx, kubeClient, globalCredsKey, namespace) - Expect(err).NotTo(HaveOccurred()) - Expect( - kubeClient.Patch(ctx, credentialsSecret, client.Apply, client.ForceOwnership, GinkGoFieldOwner), - ).To(Succeed()) -} - -// applyYAMLToNamespace applies YAML objects to a namespace after replacing placeholders. -// Returns the list of applied objects. -func applyYAMLToNamespace(ctx context.Context, kubeClient client.Client, yaml []byte, placeholders yamlPlaceholders, namespace string) []client.Object { - yamlStr := replaceYAMLPlaceholders(string(yaml), placeholders) - objs := yml.MustParseObjects(strings.NewReader(yamlStr)) - for _, obj := range objs { - obj.SetNamespace(namespace) - Expect( - kubeClient.Patch(ctx, obj, client.Apply, client.ForceOwnership, GinkGoFieldOwner), - ).To(Succeed()) - } - return objs -} - -// waitForResourceReady waits for a resource to have Ready condition set to True. -func waitForResourceReady(ctx context.Context, kubeClient client.Client, obj kube.ObjectWithStatus) { - Eventually(func(g Gomega) bool { - g.Expect( - kubeClient.Get(ctx, client.ObjectKeyFromObject(obj), obj), - ).To(Succeed()) - if condition := meta.FindStatusCondition(obj.GetConditions(), "Ready"); condition != nil { - return condition.Status == metav1.ConditionTrue - } - return false - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).To(BeTrue()) -} - -// waitForResourceUpdated waits for a resource to be Ready and in Updated state. -func waitForResourceUpdated(ctx context.Context, kubeClient client.Client, obj kube.ObjectWithStatus) { - Eventually(func(g Gomega) bool { - g.Expect( - kubeClient.Get(ctx, client.ObjectKeyFromObject(obj), obj), - ).To(Succeed()) - ready := false - if condition := meta.FindStatusCondition(obj.GetConditions(), "Ready"); condition != nil { - ready = (condition.Status == metav1.ConditionTrue) - } - if ready { - if condition := meta.FindStatusCondition(obj.GetConditions(), "State"); condition != nil { - return state.ResourceState(condition.Reason) == state.StateUpdated - } - } - return false - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).To(BeTrue()) -} - -// waitForResourceDeleted waits for a resource to be deleted from the cluster. -func waitForResourceDeleted(ctx context.Context, kubeClient client.Client, obj client.Object) { - Eventually(func(g Gomega) error { - return kubeClient.Get(ctx, client.ObjectKeyFromObject(obj), obj) - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).ShouldNot(Succeed()) -} diff --git a/test/helper/e2e2/kube/kube.go b/test/helper/e2e2/kube/kube.go index 354bae505b..a6179dc1b9 100644 --- a/test/helper/e2e2/kube/kube.go +++ b/test/helper/e2e2/kube/kube.go @@ -43,7 +43,7 @@ type ObjectWithStatus interface { } // AssertCRDNames check that the given names are CRDs installed in the accesible cluster -func AssertCRDNames(ctx context.Context, kubeClient client.Client, crdNames ... string) error { +func AssertCRDNames(ctx context.Context, kubeClient client.Client, crdNames ...string) error { crds := make([]*apiextensionsv1.CustomResourceDefinition, 0, len(crdNames)) for _, crdName := range crdNames { crd := &apiextensionsv1.CustomResourceDefinition{ diff --git a/test/helper/e2e2/resources/resources.go b/test/helper/e2e2/resources/resources.go new file mode 100644 index 0000000000..c1441f8f79 --- /dev/null +++ b/test/helper/e2e2/resources/resources.go @@ -0,0 +1,139 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package resources + +import ( + "context" + "errors" + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/state" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/kube" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/testparams" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/yml" +) + +// CopySecretToNamespace copies a secret from one namespace to another. +// Returns the copied secret ready to be applied to the target namespace. +func CopySecretToNamespace(ctx context.Context, kubeClient client.Client, key client.ObjectKey, targetNamespace string) (*corev1.Secret, error) { + secret := corev1.Secret{} + if err := kubeClient.Get(ctx, key, &secret); err != nil { + return nil, fmt.Errorf("failed to load original secret %v: %w", key, err) + } + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: targetNamespace, + Labels: secret.Labels, + }, + Data: secret.Data, + }, nil +} + +// CopyCredentialsToNamespace copies the credentials secret from the operator namespace +// to the target namespace. The secret is applied with the specified field owner. +func CopyCredentialsToNamespace(ctx context.Context, kubeClient client.Client, credentialsName, operatorNamespace, targetNamespace string, fieldOwner client.FieldOwner) error { + globalCredsKey := client.ObjectKey{ + Name: credentialsName, + Namespace: operatorNamespace, + } + credentialsSecret, err := CopySecretToNamespace(ctx, kubeClient, globalCredsKey, targetNamespace) + if err != nil { + return err + } + return kubeClient.Patch(ctx, credentialsSecret, client.Apply, client.ForceOwnership, fieldOwner) +} + +// ApplyYAMLToNamespace applies YAML objects to a namespace after replacing placeholders. +// Returns the list of applied objects. +func ApplyYAMLToNamespace(ctx context.Context, kubeClient client.Client, yaml []byte, params *testparams.TestParams, namespace string, fieldOwner client.FieldOwner) ([]client.Object, error) { + yamlStr := params.ReplaceYAML(string(yaml)) + objs := yml.MustParseObjects(strings.NewReader(yamlStr)) + for _, obj := range objs { + obj.SetNamespace(namespace) + if err := kubeClient.Patch(ctx, obj, client.Apply, client.ForceOwnership, fieldOwner); err != nil { + return nil, fmt.Errorf("failed to apply object %s/%s: %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err) + } + } + return objs, nil +} + +var ( + // ErrResourceNotReady indicates the resource is not in Ready state + ErrResourceNotReady = errors.New("resource is not ready") + // ErrResourceNotUpdated indicates the resource is not in Updated state + ErrResourceNotUpdated = errors.New("resource is not updated") + // ErrResourceNotDeleted indicates the resource still exists + ErrResourceNotDeleted = errors.New("resource still exists") +) + +// CheckResourceReady checks if a resource has Ready condition set to True. +// Returns nil if ready, ErrResourceNotReady if not ready, or an error if the resource cannot be fetched. +func CheckResourceReady(ctx context.Context, kubeClient client.Client, obj kube.ObjectWithStatus) error { + key := client.ObjectKeyFromObject(obj) + if err := kubeClient.Get(ctx, key, obj); err != nil { + return fmt.Errorf("failed to get resource %s/%s: %w", obj.GetNamespace(), obj.GetName(), err) + } + if condition := meta.FindStatusCondition(obj.GetConditions(), "Ready"); condition != nil { + if condition.Status == metav1.ConditionTrue { + return nil + } + } + return ErrResourceNotReady +} + +// CheckResourceUpdated checks if a resource is Ready and in Updated state. +// Returns nil if updated, ErrResourceNotUpdated if not updated, or an error if the resource cannot be fetched. +func CheckResourceUpdated(ctx context.Context, kubeClient client.Client, obj kube.ObjectWithStatus) error { + key := client.ObjectKeyFromObject(obj) + if err := kubeClient.Get(ctx, key, obj); err != nil { + return fmt.Errorf("failed to get resource %s/%s: %w", obj.GetNamespace(), obj.GetName(), err) + } + ready := false + if condition := meta.FindStatusCondition(obj.GetConditions(), "Ready"); condition != nil { + ready = (condition.Status == metav1.ConditionTrue) + } + if !ready { + return ErrResourceNotUpdated + } + if condition := meta.FindStatusCondition(obj.GetConditions(), "State"); condition != nil { + if state.ResourceState(condition.Reason) == state.StateUpdated { + return nil + } + } + return ErrResourceNotUpdated +} + +// CheckResourceDeleted checks if a resource has been deleted from the cluster. +// Returns nil if deleted, ErrResourceNotDeleted if still exists, or an error if the check fails. +func CheckResourceDeleted(ctx context.Context, kubeClient client.Client, obj client.Object) error { + key := client.ObjectKeyFromObject(obj) + if err := kubeClient.Get(ctx, key, obj); err != nil { + // Resource not found means it's deleted - success! + if client.IgnoreNotFound(err) == nil { + return nil + } + // Other errors are unexpected + return fmt.Errorf("failed to check if resource %s/%s is deleted: %w", obj.GetNamespace(), obj.GetName(), err) + } + return ErrResourceNotDeleted +} diff --git a/test/helper/e2e2/testparams/testparams.go b/test/helper/e2e2/testparams/testparams.go new file mode 100644 index 0000000000..29e5d3ab12 --- /dev/null +++ b/test/helper/e2e2/testparams/testparams.go @@ -0,0 +1,83 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testparams + +import "strings" + +// TestParams holds all test parameters for test isolation purposes. +// Shared values (OrgID, OperatorNamespace, CredentialsSecretName) are typically +// set from input config and remain constant across tests. Per-test values +// (GroupID, GroupName) are set per test case. +type TestParams struct { + // GroupID is the Atlas group ID, assigned by Atlas after Group creation. + // This is per-test and may be empty initially. + GroupID string + // OrgID is the Atlas organization ID, set from input config (e.g., MCLI_ORG_ID env var). + OrgID string + // GroupName is a randomized name for test isolation, per test case. + GroupName string + // OperatorNamespace is the namespace where the operator is running, set from input config. + OperatorNamespace string + // CredentialsSecretName is the name of the credentials secret, set from input config. + CredentialsSecretName string +} + +// New creates a new TestParams struct with shared configuration values. +// These values are typically constant across all tests in a suite: +// - orgID: Atlas organization ID (from MCLI_ORG_ID env var) +// - operatorNamespace: Namespace where operator is running (from OPERATOR_NAMESPACE env var) +// - credentialsSecretName: Name of the credentials secret (e.g., DefaultGlobalCredentials) +// +// Per-test values (GroupID, GroupName) should be set using WithGroupID and WithGroupName. +func New(orgID, operatorNamespace, credentialsSecretName string) *TestParams { + return &TestParams{ + OrgID: orgID, + OperatorNamespace: operatorNamespace, + CredentialsSecretName: credentialsSecretName, + } +} + +// WithGroupID returns a copy of the TestParams with GroupID set. +// GroupID is assigned by Atlas after Group creation and is per-test. +func (p *TestParams) WithGroupID(groupID string) *TestParams { + copy := *p + copy.GroupID = groupID + return © +} + +// WithGroupName returns a copy of the TestParams with GroupName set. +// GroupName should contain a randomized portion for test isolation. +func (p *TestParams) WithGroupName(groupName string) *TestParams { + copy := *p + copy.GroupName = groupName + return © +} + +// ReplaceYAML replaces all placeholders in the YAML template with actual values. +// Supported placeholders: +// - __GROUP_ID__ -> GroupID +// - __ORG_ID__ -> OrgID +// - __GROUP_NAME__ -> GroupName +// - __OPERATOR_NAMESPACE__ -> OperatorNamespace +// - __CREDENTIALS_SECRET_NAME__ -> CredentialsSecretName +func (p *TestParams) ReplaceYAML(yaml string) string { + result := yaml + result = strings.ReplaceAll(result, "__GROUP_ID__", p.GroupID) + result = strings.ReplaceAll(result, "__ORG_ID__", p.OrgID) + result = strings.ReplaceAll(result, "__GROUP_NAME__", p.GroupName) + result = strings.ReplaceAll(result, "__OPERATOR_NAMESPACE__", p.OperatorNamespace) + result = strings.ReplaceAll(result, "__CREDENTIALS_SECRET_NAME__", p.CredentialsSecretName) + return result +} From c70a78d64ebe3a56f471a3e40389a8fa33a34a47 Mon Sep 17 00:00:00 2001 From: "jose.vazquez" Date: Fri, 19 Dec 2025 14:08:31 +0100 Subject: [PATCH 4/7] Refactor YAML test sourcing --- .../atlas_generated_v1_flexcluster.yaml | 3 +- ...enerated_v1_flexcluster_with_groupref.yaml | 9 +- config/samples/atlas_generated_v1_group.yaml | 12 ++ config/samples/kustomization.yaml | 3 + test/e2e2/flexcluster_test.go | 155 ++++++++++++++---- test/e2e2/flexsamples/samples.go | 32 ---- test/e2e2/flexsamples/test_group.yaml | 13 -- .../e2e2/flexsamples/with_groupid_update.yaml | 15 -- .../flexsamples/with_groupref_update.yaml | 18 -- test/helper/e2e2/resources/resources.go | 11 +- test/helper/e2e2/samples/samples.go | 73 +++++++++ test/helper/e2e2/testparams/testparams.go | 52 ++++-- 12 files changed, 253 insertions(+), 143 deletions(-) rename test/e2e2/flexsamples/with_groupid_create.yaml => config/samples/atlas_generated_v1_flexcluster.yaml (89%) rename test/e2e2/flexsamples/with_groupref_create.yaml => config/samples/atlas_generated_v1_flexcluster_with_groupref.yaml (82%) create mode 100644 config/samples/atlas_generated_v1_group.yaml delete mode 100644 test/e2e2/flexsamples/samples.go delete mode 100644 test/e2e2/flexsamples/test_group.yaml delete mode 100644 test/e2e2/flexsamples/with_groupid_update.yaml delete mode 100644 test/e2e2/flexsamples/with_groupref_update.yaml create mode 100644 test/helper/e2e2/samples/samples.go diff --git a/test/e2e2/flexsamples/with_groupid_create.yaml b/config/samples/atlas_generated_v1_flexcluster.yaml similarity index 89% rename from test/e2e2/flexsamples/with_groupid_create.yaml rename to config/samples/atlas_generated_v1_flexcluster.yaml index d9fff213b3..ab62e54200 100644 --- a/test/e2e2/flexsamples/with_groupid_create.yaml +++ b/config/samples/atlas_generated_v1_flexcluster.yaml @@ -6,10 +6,11 @@ spec: connectionSecretRef: name: mongodb-atlas-operator-api-key v20250312: - groupId: __GROUP_ID__ + groupId: "60f1b3c4e4b0e8b8c8b8c8b" entry: name: flexy terminationProtectionEnabled: true providerSettings: backingProviderName: GCP regionName: CENTRAL_US + diff --git a/test/e2e2/flexsamples/with_groupref_create.yaml b/config/samples/atlas_generated_v1_flexcluster_with_groupref.yaml similarity index 82% rename from test/e2e2/flexsamples/with_groupref_create.yaml rename to config/samples/atlas_generated_v1_flexcluster_with_groupref.yaml index 31d8141268..f46da92f30 100644 --- a/test/e2e2/flexsamples/with_groupref_create.yaml +++ b/config/samples/atlas_generated_v1_flexcluster_with_groupref.yaml @@ -1,14 +1,14 @@ apiVersion: atlas.generated.mongodb.com/v1 kind: Group metadata: - name: __GROUP_NAME__ + name: my-group spec: connectionSecretRef: name: mongodb-atlas-operator-api-key v20250312: entry: - orgId: __ORG_ID__ - name: __GROUP_NAME__ + orgId: "60f1b3c4e4b0e8b8c8b8c8b" + name: my-group --- apiVersion: atlas.generated.mongodb.com/v1 kind: FlexCluster @@ -19,10 +19,11 @@ metadata: spec: v20250312: groupRef: - name: __GROUP_NAME__ + name: my-group entry: name: flexy terminationProtectionEnabled: true providerSettings: backingProviderName: GCP regionName: CENTRAL_US + diff --git a/config/samples/atlas_generated_v1_group.yaml b/config/samples/atlas_generated_v1_group.yaml new file mode 100644 index 0000000000..71e3d4a6ac --- /dev/null +++ b/config/samples/atlas_generated_v1_group.yaml @@ -0,0 +1,12 @@ +apiVersion: atlas.generated.mongodb.com/v1 +kind: Group +metadata: + name: my-group +spec: + connectionSecretRef: + name: mongodb-atlas-operator-api-key + v20250312: + entry: + orgId: "60f1b3c4e4b0e8b8c8b8c8b" + name: my-group + diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 240068e4b1..bf7cfdb5fb 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -20,4 +20,7 @@ resources: - atlas_v1_atlasbackupcompliancepolicy.yaml - atlas_v1_atlascustomrole.yaml - atlas_v1_atlasthirdpartyintegration.yaml + - atlas_generated_v1_group.yaml + - atlas_generated_v1_flexcluster.yaml + - atlas_generated_v1_flexcluster_with_groupref.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/test/e2e2/flexcluster_test.go b/test/e2e2/flexcluster_test.go index 40bc8e996f..166e962e04 100644 --- a/test/e2e2/flexcluster_test.go +++ b/test/e2e2/flexcluster_test.go @@ -17,7 +17,6 @@ package e2e2_test import ( "context" "os" - "strings" "time" . "github.com/onsi/ginkgo/v2" @@ -26,16 +25,16 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + k8s "github.com/crd2go/crd2go/k8s" nextapiv1 "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/version" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/e2e2/flexsamples" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/control" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e/utils" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/kube" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/operator" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/resources" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/samples" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/testparams" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/yml" ) const ( @@ -43,6 +42,12 @@ const ( GroupCRDName = "groups.atlas.generated.mongodb.com" ) +// mutationFunc is a function type for mutating objects during test setup. +type mutationFunc func(objs []client.Object, params *testparams.TestParams) *nextapiv1.FlexCluster + +// updateMutationFunc is a function type for mutating objects during test updates. +type updateMutationFunc func(cluster *nextapiv1.FlexCluster) + var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() { var ctx context.Context var kubeClient client.Client @@ -82,14 +87,15 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() By("Create test Group", func() { groupName := utils.RandomName("flexcluster-test-group") - // Set up shared test params for Group YAML template + // Set up shared test params sharedTestParams = testparams.New(orgID, sharedGroupNamespace.Name, DefaultGlobalCredentials). WithGroupName(groupName) - // Replace placeholders in the Group YAML template - groupYAML := sharedTestParams.ReplaceYAML(string(flexsamples.TestGroup)) - objs := yml.MustParseObjects(strings.NewReader(groupYAML)) + + // Load sample Group YAML and apply mutations + objs := samples.MustLoadSampleObjects("atlas_generated_v1_group.yaml") Expect(len(objs)).To(Equal(1)) testGroup = objs[0].(*nextapiv1.Group) + sharedTestParams.WithNamespace(sharedGroupNamespace.Name).ApplyToGroup(testGroup) Expect(kubeClient.Create(ctx, testGroup)).To(Succeed()) }) @@ -150,30 +156,45 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() }) DescribeTable("FlexCluster CRUD lifecycle", - func(createYAML, updateYAML []byte, clusterName string) { + func(sampleFile string, createMutation mutationFunc, updateMutation updateMutationFunc, clusterName string) { // Generate randomized group name for this test run (cluster names are unique per group) groupName := utils.RandomName("flex-grp") - // Set up test params for this test case (reuse shared values, override groupName) - testParams := sharedTestParams.WithGroupName(groupName) + // Set up test params for this test case (reuse shared values, override groupName and namespace) + testParams := sharedTestParams.WithGroupName(groupName).WithNamespace(testNamespace.Name) // Track created objects for cleanup var createdObjects []client.Object + var cluster *nextapiv1.FlexCluster By("Copy credentials secret to test namespace", func() { Expect(resources.CopyCredentialsToNamespace(ctx, kubeClient, DefaultGlobalCredentials, control.MustEnvVar("OPERATOR_NAMESPACE"), testNamespace.Name, GinkGoFieldOwner)).To(Succeed()) }) - By("Create resources from YAML", func() { - objs, err := resources.ApplyYAMLToNamespace(ctx, kubeClient, createYAML, testParams, testNamespace.Name, GinkGoFieldOwner) + By("Load sample YAML and apply mutations for create", func() { + objs := samples.MustLoadSampleObjects(sampleFile) + + // Apply create mutation function + cluster = createMutation(objs, testParams) + + // Apply all objects to namespace + createdObjects, err := resources.ApplyObjectsToNamespace(ctx, kubeClient, objs, testNamespace.Name, GinkGoFieldOwner) Expect(err).NotTo(HaveOccurred()) - createdObjects = append(createdObjects, objs...) + + // Find cluster object for later use if not returned by mutation + if cluster == nil { + for _, obj := range createdObjects { + if fc, ok := obj.(*nextapiv1.FlexCluster); ok { + cluster = fc + break + } + } + } }) By("Wait for Group to be Ready (if using groupRef)", func() { - createYAMLStr := testParams.ReplaceYAML(string(createYAML)) - objs := yml.MustParseObjects(strings.NewReader(createYAMLStr)) - for _, obj := range objs { + // Check if any Group objects were created + for _, obj := range createdObjects { if group, ok := obj.(*nextapiv1.Group); ok { groupObj := &nextapiv1.Group{ ObjectMeta: metav1.ObjectMeta{Name: group.Name, Namespace: testNamespace.Name}, @@ -185,13 +206,9 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() } }) - cluster := nextapiv1.FlexCluster{ - ObjectMeta: metav1.ObjectMeta{Name: clusterName, Namespace: testNamespace.Name}, - } - By("Wait for FlexCluster to be Ready", func() { Eventually(func(g Gomega) { - g.Expect(resources.CheckResourceReady(ctx, kubeClient, &cluster)).To(Succeed()) + g.Expect(resources.CheckResourceReady(ctx, kubeClient, cluster)).To(Succeed()) }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) }) @@ -202,18 +219,15 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() }) By("Update FlexCluster", func() { - if len(updateYAML) > 0 { - _, err := resources.ApplyYAMLToNamespace(ctx, kubeClient, updateYAML, testParams, testNamespace.Name, GinkGoFieldOwner) - Expect(err).NotTo(HaveOccurred()) - } + // Apply update mutation to the same cluster object + updateMutation(cluster) + Expect(kubeClient.Patch(ctx, cluster, client.Apply, client.ForceOwnership, GinkGoFieldOwner)).To(Succeed()) }) By("Wait for FlexCluster to be Ready & updated", func() { - if len(updateYAML) > 0 { - Eventually(func(g Gomega) { - g.Expect(resources.CheckResourceUpdated(ctx, kubeClient, &cluster)).To(Succeed()) - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) - } + Eventually(func(g Gomega) { + g.Expect(resources.CheckResourceUpdated(ctx, kubeClient, cluster)).To(Succeed()) + }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) }) By("Delete all created resources", func() { @@ -231,14 +245,87 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() }) }, Entry("With direct groupId", - flexsamples.WithGroupIdCreate, - flexsamples.WithGroupIdUpdate, + "atlas_generated_v1_flexcluster.yaml", + mutateFlexClusterWithGroupId, + updateFlexClusterTerminationProtection, "flexy", ), Entry("With groupRef", - flexsamples.WithGroupRefCreate, - flexsamples.WithGroupRefUpdate, + "atlas_generated_v1_flexcluster_with_groupref.yaml", + mutateFlexClusterWithGroupRef, + updateFlexClusterTerminationProtection, "flexy", ), ) }) + +// mutateFlexClusterWithGroupId mutates a FlexCluster object to use direct groupId. +// Returns the mutated FlexCluster if found, nil otherwise. +func mutateFlexClusterWithGroupId(objs []client.Object, params *testparams.TestParams) *nextapiv1.FlexCluster { + for _, obj := range objs { + if cluster, ok := obj.(*nextapiv1.FlexCluster); ok { + cluster.SetNamespace(params.Namespace) + + if cluster.Spec.ConnectionSecretRef == nil { + cluster.Spec.ConnectionSecretRef = &k8s.LocalReference{} + } + cluster.Spec.ConnectionSecretRef.Name = params.CredentialsSecretName + + if cluster.Spec.V20250312 == nil { + cluster.Spec.V20250312 = &nextapiv1.FlexClusterSpecV20250312{} + } + if params.GroupID != "" { + cluster.Spec.V20250312.GroupId = ¶ms.GroupID + // Clear groupRef if groupId is set + cluster.Spec.V20250312.GroupRef = nil + } + return cluster + } + } + return nil +} + +// mutateFlexClusterWithGroupRef mutates a FlexCluster object to use groupRef. +// This also mutates any Group objects in the same list to use test params. +// Returns the mutated FlexCluster if found, nil otherwise. +func mutateFlexClusterWithGroupRef(objs []client.Object, params *testparams.TestParams) *nextapiv1.FlexCluster { + var cluster *nextapiv1.FlexCluster + for _, obj := range objs { + switch o := obj.(type) { + case *nextapiv1.Group: + params.ApplyToGroup(o) + case *nextapiv1.FlexCluster: + o.SetNamespace(params.Namespace) + + if o.Spec.ConnectionSecretRef == nil { + o.Spec.ConnectionSecretRef = &k8s.LocalReference{} + } + o.Spec.ConnectionSecretRef.Name = params.CredentialsSecretName + + if o.Spec.V20250312 == nil { + o.Spec.V20250312 = &nextapiv1.FlexClusterSpecV20250312{} + } + if o.Spec.V20250312.GroupRef == nil { + o.Spec.V20250312.GroupRef = &k8s.LocalReference{} + } + o.Spec.V20250312.GroupRef.Name = params.GroupName + // Clear groupId if groupRef is set + o.Spec.V20250312.GroupId = nil + cluster = o + } + } + return cluster +} + +// updateFlexClusterTerminationProtection mutates a FlexCluster for the update scenario. +// This changes terminationProtectionEnabled from true to false. +func updateFlexClusterTerminationProtection(cluster *nextapiv1.FlexCluster) { + if cluster.Spec.V20250312 == nil { + cluster.Spec.V20250312 = &nextapiv1.FlexClusterSpecV20250312{} + } + if cluster.Spec.V20250312.Entry == nil { + cluster.Spec.V20250312.Entry = &nextapiv1.FlexClusterSpecV20250312Entry{} + } + terminationProtectionEnabled := false + cluster.Spec.V20250312.Entry.TerminationProtectionEnabled = &terminationProtectionEnabled +} diff --git a/test/e2e2/flexsamples/samples.go b/test/e2e2/flexsamples/samples.go deleted file mode 100644 index 6c5edd55db..0000000000 --- a/test/e2e2/flexsamples/samples.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2025 MongoDB Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package flexsamples - -import _ "embed" - -//go:embed with_groupid_create.yaml -var WithGroupIdCreate []byte - -//go:embed with_groupid_update.yaml -var WithGroupIdUpdate []byte - -//go:embed with_groupref_create.yaml -var WithGroupRefCreate []byte - -//go:embed with_groupref_update.yaml -var WithGroupRefUpdate []byte - -//go:embed test_group.yaml -var TestGroup []byte diff --git a/test/e2e2/flexsamples/test_group.yaml b/test/e2e2/flexsamples/test_group.yaml deleted file mode 100644 index df930bd6e3..0000000000 --- a/test/e2e2/flexsamples/test_group.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: atlas.generated.mongodb.com/v1 -kind: Group -metadata: - name: __GROUP_NAME__ - namespace: __OPERATOR_NAMESPACE__ -spec: - connectionSecretRef: - name: __CREDENTIALS_SECRET_NAME__ - v20250312: - entry: - orgId: __ORG_ID__ - name: __GROUP_NAME__ - diff --git a/test/e2e2/flexsamples/with_groupid_update.yaml b/test/e2e2/flexsamples/with_groupid_update.yaml deleted file mode 100644 index 66f1902dc3..0000000000 --- a/test/e2e2/flexsamples/with_groupid_update.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: atlas.generated.mongodb.com/v1 -kind: FlexCluster -metadata: - name: flexy -spec: - connectionSecretRef: - name: mongodb-atlas-operator-api-key - v20250312: - groupId: __GROUP_ID__ - entry: - name: flexy - terminationProtectionEnabled: false - providerSettings: - backingProviderName: GCP - regionName: CENTRAL_US diff --git a/test/e2e2/flexsamples/with_groupref_update.yaml b/test/e2e2/flexsamples/with_groupref_update.yaml deleted file mode 100644 index d6269beb0d..0000000000 --- a/test/e2e2/flexsamples/with_groupref_update.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: atlas.generated.mongodb.com/v1 -kind: FlexCluster -metadata: - name: flexy - annotations: - some-tag: tag -spec: - connectionSecretRef: - name: mongodb-atlas-operator-api-key - v20250312: - groupRef: - name: __GROUP_NAME__ - entry: - name: flexy - terminationProtectionEnabled: false - providerSettings: - backingProviderName: GCP - regionName: CENTRAL_US diff --git a/test/helper/e2e2/resources/resources.go b/test/helper/e2e2/resources/resources.go index c1441f8f79..eebe045a82 100644 --- a/test/helper/e2e2/resources/resources.go +++ b/test/helper/e2e2/resources/resources.go @@ -18,7 +18,6 @@ import ( "context" "errors" "fmt" - "strings" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" @@ -27,8 +26,6 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/state" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/kube" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/testparams" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/yml" ) // CopySecretToNamespace copies a secret from one namespace to another. @@ -63,11 +60,9 @@ func CopyCredentialsToNamespace(ctx context.Context, kubeClient client.Client, c return kubeClient.Patch(ctx, credentialsSecret, client.Apply, client.ForceOwnership, fieldOwner) } -// ApplyYAMLToNamespace applies YAML objects to a namespace after replacing placeholders. -// Returns the list of applied objects. -func ApplyYAMLToNamespace(ctx context.Context, kubeClient client.Client, yaml []byte, params *testparams.TestParams, namespace string, fieldOwner client.FieldOwner) ([]client.Object, error) { - yamlStr := params.ReplaceYAML(string(yaml)) - objs := yml.MustParseObjects(strings.NewReader(yamlStr)) +// ApplyObjectsToNamespace applies a list of objects to a namespace. +// All objects will be set to the specified namespace before applying. +func ApplyObjectsToNamespace(ctx context.Context, kubeClient client.Client, objs []client.Object, namespace string, fieldOwner client.FieldOwner) ([]client.Object, error) { for _, obj := range objs { obj.SetNamespace(namespace) if err := kubeClient.Patch(ctx, obj, client.Apply, client.ForceOwnership, fieldOwner); err != nil { diff --git a/test/helper/e2e2/samples/samples.go b/test/helper/e2e2/samples/samples.go new file mode 100644 index 0000000000..dfc97f96e4 --- /dev/null +++ b/test/helper/e2e2/samples/samples.go @@ -0,0 +1,73 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package samples + +import ( + "os" + "path/filepath" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e2/yml" +) + +// findRepoRoot finds the repository root by looking for go.mod file. +func findRepoRoot() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir, nil + } + parent := filepath.Dir(dir) + if parent == dir { + // Reached filesystem root + break + } + dir = parent + } + + // Fallback: try current directory + return ".", nil +} + +// LoadSampleObjects loads and parses YAML objects from config/samples. +// It finds the repository root and loads files from there. +func LoadSampleObjects(filename string) ([]client.Object, error) { + repoRoot, err := findRepoRoot() + if err != nil { + return nil, err + } + + absPath := filepath.Join(repoRoot, "config", "samples", filename) + f, err := os.Open(absPath) + if err != nil { + return nil, err + } + defer f.Close() + return yml.ParseObjects(f) +} + +// MustLoadSampleObjects loads and parses YAML objects, panicking on error. +func MustLoadSampleObjects(filename string) []client.Object { + objs, err := LoadSampleObjects(filename) + if err != nil { + panic(err) + } + return objs +} diff --git a/test/helper/e2e2/testparams/testparams.go b/test/helper/e2e2/testparams/testparams.go index 29e5d3ab12..72e7bc77d5 100644 --- a/test/helper/e2e2/testparams/testparams.go +++ b/test/helper/e2e2/testparams/testparams.go @@ -14,12 +14,15 @@ package testparams -import "strings" +import ( + k8s "github.com/crd2go/crd2go/k8s" + nextapiv1 "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1" +) // TestParams holds all test parameters for test isolation purposes. // Shared values (OrgID, OperatorNamespace, CredentialsSecretName) are typically // set from input config and remain constant across tests. Per-test values -// (GroupID, GroupName) are set per test case. +// (GroupID, GroupName, Namespace) are set per test case. type TestParams struct { // GroupID is the Atlas group ID, assigned by Atlas after Group creation. // This is per-test and may be empty initially. @@ -28,6 +31,8 @@ type TestParams struct { OrgID string // GroupName is a randomized name for test isolation, per test case. GroupName string + // Namespace is the Kubernetes namespace where test resources are created, per test case. + Namespace string // OperatorNamespace is the namespace where the operator is running, set from input config. OperatorNamespace string // CredentialsSecretName is the name of the credentials secret, set from input config. @@ -40,7 +45,7 @@ type TestParams struct { // - operatorNamespace: Namespace where operator is running (from OPERATOR_NAMESPACE env var) // - credentialsSecretName: Name of the credentials secret (e.g., DefaultGlobalCredentials) // -// Per-test values (GroupID, GroupName) should be set using WithGroupID and WithGroupName. +// Per-test values (GroupID, GroupName, Namespace) should be set using WithGroupID, WithGroupName, and WithNamespace. func New(orgID, operatorNamespace, credentialsSecretName string) *TestParams { return &TestParams{ OrgID: orgID, @@ -65,19 +70,30 @@ func (p *TestParams) WithGroupName(groupName string) *TestParams { return © } -// ReplaceYAML replaces all placeholders in the YAML template with actual values. -// Supported placeholders: -// - __GROUP_ID__ -> GroupID -// - __ORG_ID__ -> OrgID -// - __GROUP_NAME__ -> GroupName -// - __OPERATOR_NAMESPACE__ -> OperatorNamespace -// - __CREDENTIALS_SECRET_NAME__ -> CredentialsSecretName -func (p *TestParams) ReplaceYAML(yaml string) string { - result := yaml - result = strings.ReplaceAll(result, "__GROUP_ID__", p.GroupID) - result = strings.ReplaceAll(result, "__ORG_ID__", p.OrgID) - result = strings.ReplaceAll(result, "__GROUP_NAME__", p.GroupName) - result = strings.ReplaceAll(result, "__OPERATOR_NAMESPACE__", p.OperatorNamespace) - result = strings.ReplaceAll(result, "__CREDENTIALS_SECRET_NAME__", p.CredentialsSecretName) - return result +// WithNamespace returns a copy of the TestParams with Namespace set. +// Namespace is the Kubernetes namespace where test resources are created. +func (p *TestParams) WithNamespace(namespace string) *TestParams { + copy := *p + copy.Namespace = namespace + return © +} + +// ApplyToGroup mutates a Group object with test parameters. +func (p *TestParams) ApplyToGroup(group *nextapiv1.Group) { + group.SetNamespace(p.Namespace) + group.SetName(p.GroupName) + + if group.Spec.ConnectionSecretRef == nil { + group.Spec.ConnectionSecretRef = &k8s.LocalReference{} + } + group.Spec.ConnectionSecretRef.Name = p.CredentialsSecretName + + if group.Spec.V20250312 == nil { + group.Spec.V20250312 = &nextapiv1.V20250312{} + } + if group.Spec.V20250312.Entry == nil { + group.Spec.V20250312.Entry = &nextapiv1.Entry{} + } + group.Spec.V20250312.Entry.OrgId = p.OrgID + group.Spec.V20250312.Entry.Name = p.GroupName } From 447d4f161cb7d88c7399dafb07f663e9d8d6581e Mon Sep 17 00:00:00 2001 From: "jose.vazquez" Date: Fri, 19 Dec 2025 14:08:54 +0100 Subject: [PATCH 5/7] bugfix --- .../controller/flexcluster/handler_v20250312_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/generated/controller/flexcluster/handler_v20250312_test.go b/internal/generated/controller/flexcluster/handler_v20250312_test.go index d89d15a6a9..1f9deac3d7 100644 --- a/internal/generated/controller/flexcluster/handler_v20250312_test.go +++ b/internal/generated/controller/flexcluster/handler_v20250312_test.go @@ -1114,7 +1114,8 @@ func withGeneration(flexCluster *akov2generated.FlexCluster, generation int64) * // withObservedGeneration sets the observed generation in status conditions func withObservedGeneration(flexCluster *akov2generated.FlexCluster, observedGen int64) *akov2generated.FlexCluster { if flexCluster.Status.Conditions == nil { - flexCluster.Status.Conditions = &[]metav1.Condition{} + // Allocate a new empty slice pointer to avoid storing a pointer to a temporary value + flexCluster.Status.Conditions = new([]metav1.Condition) } conditions := *flexCluster.Status.Conditions conditions = append(conditions, metav1.Condition{ @@ -1122,10 +1123,11 @@ func withObservedGeneration(flexCluster *akov2generated.FlexCluster, observedGen ObservedGeneration: observedGen, Status: metav1.ConditionTrue, }) - // Allocate a new slice to avoid storing a pointer to a local variable + // Allocate a new slice pointer that persists beyond function scope newConditions := make([]metav1.Condition, len(conditions)) copy(newConditions, conditions) - flexCluster.Status.Conditions = &newConditions + flexCluster.Status.Conditions = new([]metav1.Condition) + *flexCluster.Status.Conditions = newConditions return flexCluster } From b2948a934d6dd529f250284c955bedd619c03e22 Mon Sep 17 00:00:00 2001 From: "jose.vazquez" Date: Fri, 19 Dec 2025 16:58:03 +0100 Subject: [PATCH 6/7] Fix e2e2 test Signed-off-by: jose.vazquez --- ...enerated_v1_flexcluster_with_groupref.yaml | 10 +- .../flexcluster/handler_v20250312.go | 47 ++++++- test/e2e2/flexcluster_test.go | 133 ++++++++---------- test/helper/e2e2/operator/embedded.go | 1 - test/helper/e2e2/operator/operator.go | 11 ++ test/helper/e2e2/samples/samples.go | 2 +- test/helper/e2e2/testparams/testparams.go | 1 + 7 files changed, 124 insertions(+), 81 deletions(-) diff --git a/config/samples/atlas_generated_v1_flexcluster_with_groupref.yaml b/config/samples/atlas_generated_v1_flexcluster_with_groupref.yaml index f46da92f30..b69b42c0c7 100644 --- a/config/samples/atlas_generated_v1_flexcluster_with_groupref.yaml +++ b/config/samples/atlas_generated_v1_flexcluster_with_groupref.yaml @@ -1,27 +1,27 @@ apiVersion: atlas.generated.mongodb.com/v1 kind: Group metadata: - name: my-group + name: my-group-for-flexcluster spec: connectionSecretRef: name: mongodb-atlas-operator-api-key v20250312: entry: orgId: "60f1b3c4e4b0e8b8c8b8c8b" - name: my-group + name: my-group-for-flexcluster --- apiVersion: atlas.generated.mongodb.com/v1 kind: FlexCluster metadata: - name: flexy + name: flexy-with-groupref annotations: some-tag: tag spec: v20250312: groupRef: - name: my-group + name: my-group-for-flexcluster entry: - name: flexy + name: flexy-with-groupref terminationProtectionEnabled: true providerSettings: backingProviderName: GCP diff --git a/internal/generated/controller/flexcluster/handler_v20250312.go b/internal/generated/controller/flexcluster/handler_v20250312.go index dcfabd9d49..9ad18382ee 100644 --- a/internal/generated/controller/flexcluster/handler_v20250312.go +++ b/internal/generated/controller/flexcluster/handler_v20250312.go @@ -20,6 +20,7 @@ import ( "fmt" v20250312sdk "go.mongodb.org/atlas-sdk/v20250312009/admin" + apierrors "k8s.io/apimachinery/pkg/api/errors" controllerruntime "sigs.k8s.io/controller-runtime" builder "sigs.k8s.io/controller-runtime/pkg/builder" client "sigs.k8s.io/controller-runtime/pkg/client" @@ -71,6 +72,26 @@ func (h *Handlerv20250312) getDependencies(ctx context.Context, flexcluster *ako return result, nil } +// getMinimalGroupFromStatusOrSpec creates a minimal Group object with group ID from status (preferred) or spec (fallback). +// Returns nil if no group ID is available. This allows deletion to proceed even if the Group CR is gone from Kubernetes. +func (h *Handlerv20250312) getMinimalGroupFromStatusOrSpec(flexcluster *akov2generated.FlexCluster) *akov2generated.Group { + var groupID *string + if flexcluster.Status.V20250312 != nil { + groupID = flexcluster.Status.V20250312.GroupId + } + if groupID == nil && flexcluster.Spec.V20250312 != nil { + groupID = flexcluster.Spec.V20250312.GroupId + } + if groupID == nil || *groupID == "" { + return nil + } + return &akov2generated.Group{ + Status: akov2generated.GroupStatus{ + V20250312: &akov2generated.GroupStatusV20250312{Id: groupID}, + }, + } +} + // HandleInitial handles the initial state for version v20250312 func (h *Handlerv20250312) HandleInitial(ctx context.Context, flexcluster *akov2generated.FlexCluster) (ctrlstate.Result, error) { deps, err := h.getDependencies(ctx, flexcluster) @@ -165,7 +186,18 @@ func (h *Handlerv20250312) HandleDeletionRequested(ctx context.Context, flexclus deps, err := h.getDependencies(ctx, flexcluster) if err != nil { - return result.Error(state.StateDeletionRequested, fmt.Errorf("failed to get dependencies: %w", err)) + // Race condition: Group CR may be deleted from K8s before FlexCluster finishes deletion. + // If Group is not found but we have group ID in status, use it to proceed with deletion. + var statusErr *apierrors.StatusError + if errors.As(err, &statusErr) && apierrors.IsNotFound(statusErr) { + if group := h.getMinimalGroupFromStatusOrSpec(flexcluster); group != nil { + deps = []client.Object{group} + } else { + return result.Error(state.StateDeletionRequested, fmt.Errorf("failed to get dependencies: %w", err)) + } + } else { + return result.Error(state.StateDeletionRequested, fmt.Errorf("failed to get dependencies: %w", err)) + } } params := &v20250312sdk.DeleteFlexClusterApiParams{} @@ -189,7 +221,18 @@ func (h *Handlerv20250312) HandleDeletionRequested(ctx context.Context, flexclus func (h *Handlerv20250312) HandleDeleting(ctx context.Context, flexcluster *akov2generated.FlexCluster) (ctrlstate.Result, error) { deps, err := h.getDependencies(ctx, flexcluster) if err != nil { - return result.Error(state.StateDeleting, fmt.Errorf("failed to get dependencies: %w", err)) + // Race condition: Group CR may be deleted from K8s before FlexCluster finishes deletion. + // If Group is not found but we have group ID in status, use it to proceed with deletion. + var statusErr *apierrors.StatusError + if errors.As(err, &statusErr) && apierrors.IsNotFound(statusErr) { + if group := h.getMinimalGroupFromStatusOrSpec(flexcluster); group != nil { + deps = []client.Object{group} + } else { + return result.Error(state.StateDeleting, fmt.Errorf("failed to get dependencies: %w", err)) + } + } else { + return result.Error(state.StateDeleting, fmt.Errorf("failed to get dependencies: %w", err)) + } } params := &v20250312sdk.GetFlexClusterApiParams{} diff --git a/test/e2e2/flexcluster_test.go b/test/e2e2/flexcluster_test.go index 166e962e04..0676a48a1c 100644 --- a/test/e2e2/flexcluster_test.go +++ b/test/e2e2/flexcluster_test.go @@ -19,14 +19,15 @@ import ( "os" "time" + k8s "github.com/crd2go/crd2go/k8s" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" - k8s "github.com/crd2go/crd2go/k8s" nextapiv1 "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/version" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/control" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e/utils" @@ -42,14 +43,13 @@ const ( GroupCRDName = "groups.atlas.generated.mongodb.com" ) -// mutationFunc is a function type for mutating objects during test setup. -type mutationFunc func(objs []client.Object, params *testparams.TestParams) *nextapiv1.FlexCluster +// prepareFunc is a function type for mutating objects during test setup. +type prepareFunc func(objs []client.Object, params *testparams.TestParams) *nextapiv1.FlexCluster -// updateMutationFunc is a function type for mutating objects during test updates. -type updateMutationFunc func(cluster *nextapiv1.FlexCluster) +// updateFunc is a function type for mutating objects during test updates. +type updateFunc func(cluster *nextapiv1.FlexCluster) var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() { - var ctx context.Context var kubeClient client.Client var ako operator.Operator var testNamespace *corev1.Namespace @@ -59,7 +59,7 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() var orgID string var sharedTestParams *testparams.TestParams - _ = BeforeAll(func() { + _ = BeforeAll(func(ctx context.Context) { if !version.IsExperimental() { Skip("FlexCluster is an experimental CRD and controller. Skipping test as experimental features are not enabled.") } @@ -67,11 +67,11 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() orgID = os.Getenv("MCLI_ORG_ID") Expect(orgID).NotTo(BeEmpty(), "MCLI_ORG_ID environment variable must be set") + // Start operator deletionProtectionOff := false ako = runTestAKO(DefaultGlobalCredentials, control.MustEnvVar("OPERATOR_NAMESPACE"), deletionProtectionOff) ako.Start(GinkgoT()) - ctx = context.Background() testClient, err := kube.NewTestClient() Expect(err).To(Succeed()) kubeClient = testClient @@ -102,7 +102,7 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() By("Wait for Group to be Ready and get its ID", func() { Eventually(func(g Gomega) { g.Expect(resources.CheckResourceReady(ctx, kubeClient, testGroup)).To(Succeed()) - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) + }).WithContext(ctx).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) Expect(testGroup.Status.V20250312).NotTo(BeNil()) Expect(testGroup.Status.V20250312.Id).NotTo(BeNil()) groupID = *testGroup.Status.V20250312.Id @@ -112,14 +112,14 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() }) }) - _ = AfterAll(func() { + _ = AfterAll(func(ctx context.Context) { if kubeClient != nil && testGroup != nil { By("Clean up test Group", func() { Expect(kubeClient.Delete(ctx, testGroup)).To(Succeed()) Eventually(func(g Gomega) error { err := kubeClient.Get(ctx, client.ObjectKeyFromObject(testGroup), testGroup) return err - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).NotTo(Succeed()) + }).WithContext(ctx).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).NotTo(Succeed()) }) } if kubeClient != nil && sharedGroupNamespace != nil { @@ -127,7 +127,7 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() Expect(kubeClient.Delete(ctx, sharedGroupNamespace)).To(Succeed()) Eventually(func(g Gomega) bool { return kubeClient.Get(ctx, client.ObjectKeyFromObject(sharedGroupNamespace), sharedGroupNamespace) == nil - }).WithTimeout(time.Minute).WithPolling(time.Second).To(BeFalse()) + }).WithContext(ctx).WithTimeout(time.Minute).WithPolling(time.Second).To(BeFalse()) }) } if ako != nil { @@ -135,7 +135,7 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() } }) - _ = BeforeEach(func() { + _ = BeforeEach(func(ctx context.Context) { testNamespace = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ Name: utils.RandomName("flexcluster-ctlr-ns"), }} @@ -143,7 +143,7 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() Expect(ako.Running()).To(BeTrue(), "Operator must be running") }) - _ = AfterEach(func() { + _ = AfterEach(func(ctx context.Context) { if kubeClient == nil { return } @@ -152,11 +152,11 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() ).To(Succeed()) Eventually(func(g Gomega) bool { return kubeClient.Get(ctx, client.ObjectKeyFromObject(testNamespace), testNamespace) == nil - }).WithTimeout(time.Minute).WithPolling(time.Second).To(BeFalse()) + }).WithContext(ctx).WithTimeout(time.Minute).WithPolling(time.Second).To(BeFalse()) }) DescribeTable("FlexCluster CRUD lifecycle", - func(sampleFile string, createMutation mutationFunc, updateMutation updateMutationFunc, clusterName string) { + func(ctx SpecContext, sampleFile string, createMutation prepareFunc, updateMutation updateFunc, clusterName string) { // Generate randomized group name for this test run (cluster names are unique per group) groupName := utils.RandomName("flex-grp") @@ -176,20 +176,14 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() // Apply create mutation function cluster = createMutation(objs, testParams) + Expect(cluster).NotTo(BeNil()) // Apply all objects to namespace - createdObjects, err := resources.ApplyObjectsToNamespace(ctx, kubeClient, objs, testNamespace.Name, GinkGoFieldOwner) + var err error + createdObjects, err = resources.ApplyObjectsToNamespace(ctx, kubeClient, objs, testNamespace.Name, GinkGoFieldOwner) Expect(err).NotTo(HaveOccurred()) - - // Find cluster object for later use if not returned by mutation - if cluster == nil { - for _, obj := range createdObjects { - if fc, ok := obj.(*nextapiv1.FlexCluster); ok { - cluster = fc - break - } - } - } + Expect(createdObjects).NotTo(BeEmpty()) + Expect(createdObjects).To(ContainElement(cluster)) }) By("Wait for Group to be Ready (if using groupRef)", func() { @@ -201,7 +195,7 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() } Eventually(func(g Gomega) { g.Expect(resources.CheckResourceReady(ctx, kubeClient, groupObj)).To(Succeed()) - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) + }).WithContext(ctx).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) } } }) @@ -209,7 +203,7 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() By("Wait for FlexCluster to be Ready", func() { Eventually(func(g Gomega) { g.Expect(resources.CheckResourceReady(ctx, kubeClient, cluster)).To(Succeed()) - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) + }).WithContext(ctx).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) }) By("Verify cluster was created", func() { @@ -219,15 +213,20 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() }) By("Update FlexCluster", func() { - // Apply update mutation to the same cluster object - updateMutation(cluster) - Expect(kubeClient.Patch(ctx, cluster, client.Apply, client.ForceOwnership, GinkGoFieldOwner)).To(Succeed()) + // Create a fresh object for SSA (like kubectl apply -f) - no managedFields + // This simulates applying a fresh YAML file + updatedCluster := freshFlexCluster(cluster) + updateMutation(updatedCluster) + // Use SSA to simulate kubectl apply -f + Expect(kubeClient.Patch(ctx, updatedCluster, client.Apply, client.ForceOwnership, GinkGoFieldOwner)).To(Succeed()) + // Update cluster reference for subsequent checks + cluster = updatedCluster }) By("Wait for FlexCluster to be Ready & updated", func() { Eventually(func(g Gomega) { g.Expect(resources.CheckResourceUpdated(ctx, kubeClient, cluster)).To(Succeed()) - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) + }).WithContext(ctx).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) }) By("Delete all created resources", func() { @@ -240,55 +239,46 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() for _, obj := range createdObjects { Eventually(func(g Gomega) { g.Expect(resources.CheckResourceDeleted(ctx, kubeClient, obj)).To(Succeed()) - }).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) + }).WithContext(ctx).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed()) } }) }, Entry("With direct groupId", "atlas_generated_v1_flexcluster.yaml", - mutateFlexClusterWithGroupId, + prepareFlexClusterWithGroupId, updateFlexClusterTerminationProtection, "flexy", ), Entry("With groupRef", "atlas_generated_v1_flexcluster_with_groupref.yaml", - mutateFlexClusterWithGroupRef, + prepareFlexClusterWithGroupRef, updateFlexClusterTerminationProtection, "flexy", ), ) }) -// mutateFlexClusterWithGroupId mutates a FlexCluster object to use direct groupId. +// prepareFlexClusterWithGroupId prepares a FlexCluster object to use direct groupId. // Returns the mutated FlexCluster if found, nil otherwise. -func mutateFlexClusterWithGroupId(objs []client.Object, params *testparams.TestParams) *nextapiv1.FlexCluster { +func prepareFlexClusterWithGroupId(objs []client.Object, params *testparams.TestParams) *nextapiv1.FlexCluster { for _, obj := range objs { if cluster, ok := obj.(*nextapiv1.FlexCluster); ok { cluster.SetNamespace(params.Namespace) - - if cluster.Spec.ConnectionSecretRef == nil { - cluster.Spec.ConnectionSecretRef = &k8s.LocalReference{} - } - cluster.Spec.ConnectionSecretRef.Name = params.CredentialsSecretName - - if cluster.Spec.V20250312 == nil { - cluster.Spec.V20250312 = &nextapiv1.FlexClusterSpecV20250312{} - } - if params.GroupID != "" { - cluster.Spec.V20250312.GroupId = ¶ms.GroupID - // Clear groupRef if groupId is set - cluster.Spec.V20250312.GroupRef = nil + cluster.Spec.ConnectionSecretRef = &k8s.LocalReference{ + Name: params.CredentialsSecretName, } + cluster.Spec.V20250312.GroupId = ¶ms.GroupID + cluster.Spec.V20250312.GroupRef = nil return cluster } } return nil } -// mutateFlexClusterWithGroupRef mutates a FlexCluster object to use groupRef. +// prepareFlexClusterWithGroupRef prepares a FlexCluster object to use groupRef. // This also mutates any Group objects in the same list to use test params. // Returns the mutated FlexCluster if found, nil otherwise. -func mutateFlexClusterWithGroupRef(objs []client.Object, params *testparams.TestParams) *nextapiv1.FlexCluster { +func prepareFlexClusterWithGroupRef(objs []client.Object, params *testparams.TestParams) *nextapiv1.FlexCluster { var cluster *nextapiv1.FlexCluster for _, obj := range objs { switch o := obj.(type) { @@ -296,20 +286,12 @@ func mutateFlexClusterWithGroupRef(objs []client.Object, params *testparams.Test params.ApplyToGroup(o) case *nextapiv1.FlexCluster: o.SetNamespace(params.Namespace) - - if o.Spec.ConnectionSecretRef == nil { - o.Spec.ConnectionSecretRef = &k8s.LocalReference{} + o.Spec.ConnectionSecretRef = &k8s.LocalReference{ + Name: params.CredentialsSecretName, } - o.Spec.ConnectionSecretRef.Name = params.CredentialsSecretName - - if o.Spec.V20250312 == nil { - o.Spec.V20250312 = &nextapiv1.FlexClusterSpecV20250312{} + o.Spec.V20250312.GroupRef = &k8s.LocalReference{ + Name: params.GroupName, } - if o.Spec.V20250312.GroupRef == nil { - o.Spec.V20250312.GroupRef = &k8s.LocalReference{} - } - o.Spec.V20250312.GroupRef.Name = params.GroupName - // Clear groupId if groupRef is set o.Spec.V20250312.GroupId = nil cluster = o } @@ -320,12 +302,19 @@ func mutateFlexClusterWithGroupRef(objs []client.Object, params *testparams.Test // updateFlexClusterTerminationProtection mutates a FlexCluster for the update scenario. // This changes terminationProtectionEnabled from true to false. func updateFlexClusterTerminationProtection(cluster *nextapiv1.FlexCluster) { - if cluster.Spec.V20250312 == nil { - cluster.Spec.V20250312 = &nextapiv1.FlexClusterSpecV20250312{} - } - if cluster.Spec.V20250312.Entry == nil { - cluster.Spec.V20250312.Entry = &nextapiv1.FlexClusterSpecV20250312Entry{} + cluster.Spec.V20250312.Entry.TerminationProtectionEnabled = pointer.MakePtr(false) +} + +func freshFlexCluster(cluster *nextapiv1.FlexCluster) *nextapiv1.FlexCluster { + return &nextapiv1.FlexCluster{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "atlas.generated.mongodb.com/v1", + Kind: "FlexCluster", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: cluster.Name, + Namespace: cluster.Namespace, + }, + Spec: cluster.Spec, } - terminationProtectionEnabled := false - cluster.Spec.V20250312.Entry.TerminationProtectionEnabled = &terminationProtectionEnabled } diff --git a/test/helper/e2e2/operator/embedded.go b/test/helper/e2e2/operator/embedded.go index c2bc159418..eeea59f042 100644 --- a/test/helper/e2e2/operator/embedded.go +++ b/test/helper/e2e2/operator/embedded.go @@ -74,5 +74,4 @@ func (e *EmbeddedOperator) Stop(t testingT) { t.Logf("canceling operator context to force it to stop") e.cancelFn() e.Wait(t) - return } diff --git a/test/helper/e2e2/operator/operator.go b/test/helper/e2e2/operator/operator.go index b0966d9a4f..1da508fbb9 100644 --- a/test/helper/e2e2/operator/operator.go +++ b/test/helper/e2e2/operator/operator.go @@ -149,6 +149,12 @@ func (o *OperatorProcess) Wait(t testingT) { } func (o *OperatorProcess) Stop(t testingT) { + // Check if process is already terminated + if !o.Running() { + // Process has already terminated, nothing to do + return + } + // Ensure child process is killed on cleanup - send the negative of the pid, which is the process group id. // See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773 pid := 0 @@ -160,6 +166,11 @@ func (o *OperatorProcess) Stop(t testingT) { terminated := false if pid != 0 { if err := syscall.Kill(pid, syscall.SIGTERM); err != nil { + // If process doesn't exist, it's already gone - that's fine + if err == syscall.ESRCH { + // Process doesn't exist (already terminated), which is what we want + return + } t.Errorf("error trying to kill command: %v", err) } terminated = true diff --git a/test/helper/e2e2/samples/samples.go b/test/helper/e2e2/samples/samples.go index dfc97f96e4..9d005fe7ee 100644 --- a/test/helper/e2e2/samples/samples.go +++ b/test/helper/e2e2/samples/samples.go @@ -55,7 +55,7 @@ func LoadSampleObjects(filename string) ([]client.Object, error) { } absPath := filepath.Join(repoRoot, "config", "samples", filename) - f, err := os.Open(absPath) + f, err := os.Open(filepath.Clean(absPath)) if err != nil { return nil, err } diff --git a/test/helper/e2e2/testparams/testparams.go b/test/helper/e2e2/testparams/testparams.go index 72e7bc77d5..f75ac13395 100644 --- a/test/helper/e2e2/testparams/testparams.go +++ b/test/helper/e2e2/testparams/testparams.go @@ -16,6 +16,7 @@ package testparams import ( k8s "github.com/crd2go/crd2go/k8s" + nextapiv1 "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/nextapi/generated/v1" ) From 64f0fcf5c9fcfec578f42567d1242b72c5ab5cbc Mon Sep 17 00:00:00 2001 From: "jose.vazquez" Date: Mon, 22 Dec 2025 11:12:09 +0100 Subject: [PATCH 7/7] Fix e2e2 Ctrl+C cleanup --- test/e2e2/ako_test.go | 12 ++++++++---- test/e2e2/flex_to_dedicated_test.go | 14 ++++++++------ test/e2e2/flexcluster_test.go | 29 ++++++++++++++++++----------- test/e2e2/integration_test.go | 14 ++++++++------ 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/test/e2e2/ako_test.go b/test/e2e2/ako_test.go index 5d821ca5ae..46e7db0668 100644 --- a/test/e2e2/ako_test.go +++ b/test/e2e2/ako_test.go @@ -48,6 +48,14 @@ var _ = Describe("Atlas Operator Start and Stop test", Ordered, Label("ako-start ako = runTestAKO(DefaultGlobalCredentials, control.MustEnvVar("OPERATOR_NAMESPACE"), deletionProtectionOff) ako.Start(GinkgoT()) + // Register cleanup - this should even when the process is interrupted with Ctrl+C + // AfterAll is not reliable in such cases. + DeferCleanup(func() { + if ako != nil { + ako.Stop(GinkgoT()) + } + }) + ctx = context.Background() client, err := kube.NewTestClient() Expect(err).To(Succeed()) @@ -57,10 +65,6 @@ var _ = Describe("Atlas Operator Start and Stop test", Ordered, Label("ako-start })).To(Succeed()) }) - _ = AfterAll(func() { - ako.Stop(GinkgoT()) - }) - _ = BeforeEach(func() { testNamespace = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ Name: utils.RandomName("ako-ns"), diff --git a/test/e2e2/flex_to_dedicated_test.go b/test/e2e2/flex_to_dedicated_test.go index 581be1e992..c4303c61a8 100644 --- a/test/e2e2/flex_to_dedicated_test.go +++ b/test/e2e2/flex_to_dedicated_test.go @@ -51,18 +51,20 @@ var _ = Describe("Flex to Dedicated Upgrade", Ordered, Label("flex-to-dedicated" ako = runTestAKO(DefaultGlobalCredentials, control.MustEnvVar("OPERATOR_NAMESPACE"), false) ako.Start(GinkgoT()) + // Register cleanup - this should even when the process is interrupted with Ctrl+C + // AfterAll is not reliable in such cases. + DeferCleanup(func() { + if ako != nil { + ako.Stop(GinkgoT()) + } + }) + ctx = context.Background() client, err := kube.NewTestClient() Expect(err).ToNot(HaveOccurred()) kubeClient = client }) - _ = AfterAll(func() { - if ako != nil { - ako.Stop(GinkgoT()) - } - }) - _ = BeforeEach(func() { resourcePrefix = utils.RandomName("flex-to-dedicated") testNamespace = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ diff --git a/test/e2e2/flexcluster_test.go b/test/e2e2/flexcluster_test.go index 0676a48a1c..c6649fbad6 100644 --- a/test/e2e2/flexcluster_test.go +++ b/test/e2e2/flexcluster_test.go @@ -72,6 +72,14 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() ako = runTestAKO(DefaultGlobalCredentials, control.MustEnvVar("OPERATOR_NAMESPACE"), deletionProtectionOff) ako.Start(GinkgoT()) + // Register cleanup - this should even when the process is interrupted with Ctrl+C + // AfterAll is not reliable in such cases. + DeferCleanup(func() { + if ako != nil { + ako.Stop(GinkgoT()) + } + }) + testClient, err := kube.NewTestClient() Expect(err).To(Succeed()) kubeClient = testClient @@ -113,26 +121,25 @@ var _ = Describe("FlexCluster CRUD", Ordered, Label("flexcluster-ctlr"), func() }) _ = AfterAll(func(ctx context.Context) { - if kubeClient != nil && testGroup != nil { - By("Clean up test Group", func() { + By("Clean up test Group", func() { + if kubeClient != nil && testGroup != nil { + Expect(kubeClient.Delete(ctx, testGroup)).To(Succeed()) Eventually(func(g Gomega) error { err := kubeClient.Get(ctx, client.ObjectKeyFromObject(testGroup), testGroup) return err }).WithContext(ctx).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).NotTo(Succeed()) - }) - } - if kubeClient != nil && sharedGroupNamespace != nil { - By("Clean up shared group namespace", func() { + } + }) + By("Clean up shared group namespace", func() { + if kubeClient != nil && sharedGroupNamespace != nil { + Expect(kubeClient.Delete(ctx, sharedGroupNamespace)).To(Succeed()) Eventually(func(g Gomega) bool { return kubeClient.Get(ctx, client.ObjectKeyFromObject(sharedGroupNamespace), sharedGroupNamespace) == nil }).WithContext(ctx).WithTimeout(time.Minute).WithPolling(time.Second).To(BeFalse()) - }) - } - if ako != nil { - ako.Stop(GinkgoT()) - } + } + }) }) _ = BeforeEach(func(ctx context.Context) { diff --git a/test/e2e2/integration_test.go b/test/e2e2/integration_test.go index c47ed5c97f..71cea4611e 100644 --- a/test/e2e2/integration_test.go +++ b/test/e2e2/integration_test.go @@ -58,6 +58,14 @@ var _ = Describe("Atlas Third-Party Integrations Controller", Ordered, Label("in ako = runTestAKO(DefaultGlobalCredentials, control.MustEnvVar("OPERATOR_NAMESPACE"), deletionProtectionOff) ako.Start(GinkgoT()) + // Register cleanup - this should even when the process is interrupted with Ctrl+C + // AfterAll is not reliable in such cases. + DeferCleanup(func() { + if ako != nil { + ako.Stop(GinkgoT()) + } + }) + ctx = context.Background() client, err := kube.NewTestClient() Expect(err).To(Succeed()) @@ -67,12 +75,6 @@ var _ = Describe("Atlas Third-Party Integrations Controller", Ordered, Label("in })).To(Succeed()) }) - _ = AfterAll(func() { - if ako != nil { - ako.Stop(GinkgoT()) - } - }) - _ = BeforeEach(func() { testNamespace = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ Name: utils.RandomName("integrations-ctlr-ns"),