diff --git a/.github/workflows/e2e-testing.yaml b/.github/workflows/e2e-testing.yaml index 308f3b5c7..bcbbcc9f1 100644 --- a/.github/workflows/e2e-testing.yaml +++ b/.github/workflows/e2e-testing.yaml @@ -63,7 +63,41 @@ jobs: MONGODB_ATLAS_SECRET_PROFILE: cfn-cloud-dev-github-action run: | cd cfn-resources/test/e2e/cluster - go test -timeout 90m -v cluster_test.go + go test -timeout 90m -v -run '^TestClusterCFN$' . + + # Run idividual test in separate test group for parallel execution. + # Due to usage of t.Setenv() in test code t.Parallel() is not possible. + # Having both tests run in same `go test` execution is not possible due to scripts used for private registry publishing modifying same files + cluster-pause: + needs: change-detection + if: ${{ needs.change-detection.outputs.cluster == 'true' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c + with: + python-version: '3.9' + cache: 'pip' + - run: pip install cloudformation-cli-go-plugin + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 + with: + go-version-file: 'cfn-resources/go.mod' + - uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_TEST_ENV }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_TEST_ENV }} + aws-region: eu-west-1 + - name: Run E2E test + shell: bash + env: + MONGODB_ATLAS_PUBLIC_KEY: ${{ secrets.CLOUD_DEV_PUBLIC_KEY }} + MONGODB_ATLAS_PRIVATE_KEY: ${{ secrets.CLOUD_DEV_PRIVATE_KEY }} + MONGODB_ATLAS_ORG_ID: ${{ secrets.CLOUD_DEV_ORG_ID }} + MONGODB_ATLAS_BASE_URL: https://cloud-dev.mongodb.com/ + MONGODB_ATLAS_SECRET_PROFILE: cfn-cloud-dev-github-action + run: | + cd cfn-resources/test/e2e/cluster + go test -timeout 90m -v -run '^TestClusterPauseCFN$' . flex-cluster: diff --git a/cfn-resources/cluster/cmd/resource/resource.go b/cfn-resources/cluster/cmd/resource/resource.go index ffb0b89dc..d0fe60594 100644 --- a/cfn-resources/cluster/cmd/resource/resource.go +++ b/cfn-resources/cluster/cmd/resource/resource.go @@ -120,9 +120,19 @@ func Update(req handler.Request, prevModel *Model, currentModel *Model) (handler return updateClusterCallback(client, currentModel, *currentModel.ProjectId) } currentModel.validateDefaultLabel() + + currentCluster, resp, err := client.Atlas20231115014.ClustersApi.GetCluster(context.Background(), *currentModel.ProjectId, *currentModel.Name).Execute() + if pe := util.HandleClusterError(err, resp); pe != nil { + return *pe, nil + } + + // Unpausing must be handled separately from other updates to avoid errors from the API. + if pe := handleUnpausingUpdate(client, currentCluster, currentModel); pe != nil { + return *pe, nil + } + adminCluster, errEvent := setClusterRequest(currentModel) if len(adminCluster.GetReplicationSpecs()) > 0 { - currentCluster, _, _ := client.Atlas20231115014.ClustersApi.GetCluster(context.Background(), *currentModel.ProjectId, *currentModel.Name).Execute() if currentCluster != nil { adminCluster.ReplicationSpecs = AddReplicationSpecIDs(currentCluster.GetReplicationSpecs(), adminCluster.GetReplicationSpecs()) } @@ -150,6 +160,15 @@ func Update(req handler.Request, prevModel *Model, currentModel *Model) (handler return event, nil } +func handleUnpausingUpdate(client *util.MongoDBClient, currentCluster *admin20231115014.AdvancedClusterDescription, currentModel *Model) *handler.ProgressEvent { + if (currentCluster.Paused != nil && *currentCluster.Paused) && (currentModel.Paused == nil || !*currentModel.Paused) { + _, resp, err := client.Atlas20231115014.ClustersApi.UpdateCluster(context.Background(), *currentModel.ProjectId, *currentModel.Name, + &admin20231115014.AdvancedClusterDescription{Paused: admin20231115014.PtrBool(false)}).Execute() + return util.HandleClusterError(err, resp) + } + return nil +} + // Delete handles the Delete event from the Cloudformation service. func Delete(req handler.Request, prevModel *Model, currentModel *Model) (handler.ProgressEvent, error) { client, setupErr := setupRequest(req, currentModel, createReadUpdateDeleteRequiredFields) diff --git a/cfn-resources/test/e2e/cluster/cluster_pause.json.template b/cfn-resources/test/e2e/cluster/cluster_pause.json.template new file mode 100644 index 000000000..6d827a96d --- /dev/null +++ b/cfn-resources/test/e2e/cluster/cluster_pause.json.template @@ -0,0 +1,41 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Minimal template to exercise pausing/unpausing a single REPLICASET cluster", + "Resources": { + "Cluster": { + "Type": "{{ .ResourceTypeName }}", + "Properties": { + "Name": "{{ .Name }}", + "ProjectId": "{{ .ProjectID }}", + "Profile": "{{ .Profile }}", + "ClusterType": "REPLICASET", + "Paused": "{{ .Paused }}", + "ReplicationSpecs": [ + { + "NumShards": 1, + "AdvancedRegionConfigs": [ + { + "RegionName": "US_EAST_1", + "Priority": 7, + "ProviderName": "AWS", + "ElectableSpecs": { + "EbsVolumeType": "STANDARD", + "InstanceSize": "M10", + "NodeCount": 3 + } + } + ] + } + ] + } + } + }, + "Outputs": { + "MongoDBAtlasClusterID": { + "Description": "Cluster Id", + "Value": { "Fn::GetAtt": ["Cluster", "Id"] } + } + } +} + + diff --git a/cfn-resources/test/e2e/cluster/cluster_pause_test.go b/cfn-resources/test/e2e/cluster/cluster_pause_test.go new file mode 100644 index 000000000..5e4bd4f8f --- /dev/null +++ b/cfn-resources/test/e2e/cluster/cluster_pause_test.go @@ -0,0 +1,208 @@ +// Copyright 2024 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 cluster_test + +import ( + ctx "context" + "os" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/cloudformation" + "github.com/mongodb/mongodbatlas-cloudformation-resources/test/e2e/utility" + "github.com/stretchr/testify/assert" + admin20231115014 "go.mongodb.org/atlas-sdk/v20231115014/admin" +) + +type pauseTestContext struct { + cfnClient *cloudformation.Client + atlasClient20231115014 *admin20231115014.APIClient + resourceCtx utility.ResourceContext + template string + clusterTmplObj pauseTestCluster +} + +type pauseTestCluster struct { + ResourceTypeName string + Name string + Profile string + ProjectID string + Paused string +} + +const ( + pauseCfnTemplatePath = "cluster_pause.json.template" +) + +// Replication specs are hardcoded in the CFN template for this test. + +var ( + pauseProfile = os.Getenv("MONGODB_ATLAS_SECRET_PROFILE") + pauseOrgID = os.Getenv("MONGODB_ATLAS_ORG_ID") + pauseRandSuffix = utility.GetRandNum().String() + pauseProjectName = "cfn-e2e-cluster-pause" + pauseRandSuffix + pauseClusterName = "cfn-e2e-cluster-pause" + pauseRandSuffix + pauseStackName = "stack-cluster-pause-e2e-" + pauseRandSuffix +) + +func TestClusterPauseCFN(t *testing.T) { + testCtx := setupPauseSuite(t) + + t.Run("Validate Template", func(t *testing.T) { + utility.TestIsTemplateValid(t, testCtx.cfnClient, testCtx.template) + }) + + t.Run("Create Stack", func(t *testing.T) { + testCreatePauseStack(t, testCtx) + }) + + t.Run("Pause Cluster", func(t *testing.T) { + testUpdatePauseState(t, testCtx, true) + }) + + t.Run("Unpause Cluster", func(t *testing.T) { + testUpdatePauseState(t, testCtx, false) + }) + + t.Run("Delete Stack", func(t *testing.T) { + testDeletePauseStack(t, testCtx) + }) +} + +func setupPauseSuite(t *testing.T) *pauseTestContext { + t.Helper() + t.Log("Setting up pause suite") + testCtx := new(pauseTestContext) + testCtx.setUp(t) + return testCtx +} + +func (c *pauseTestContext) setUp(t *testing.T) { + t.Helper() + c.resourceCtx = utility.InitResourceCtx(pauseStackName, pauseRandSuffix, resourceTypeName, resourceDirectory) + c.cfnClient, _ = utility.NewClients(t) + _, c.atlasClient20231115014 = utility.NewClients20231115014(t) + + utility.PublishToPrivateRegistry(t, c.resourceCtx) + c.setupPrerequisites(t) +} + +func (c *pauseTestContext) setupPrerequisites(t *testing.T) { + t.Helper() + t.Cleanup(func() { + cleanupPausePrerequisites(t, c) + cleanupPauseResources(t, c) + }) + t.Log("Setting up prerequisites for pause test") + + var projectID string + if projectIDEnvVar := os.Getenv("MONGODB_ATLAS_PROJECT_ID"); projectIDEnvVar != "" { + t.Logf("using projectID from env var %s", projectIDEnvVar) + projectID = projectIDEnvVar + } else { + projectID = utility.CreateProject(t, c.atlasClient20231115014, pauseOrgID, pauseProjectName) + } + + c.clusterTmplObj = pauseTestCluster{ + Name: pauseClusterName, + ProjectID: projectID, + Profile: pauseProfile, + Paused: "false", + ResourceTypeName: os.Getenv("RESOURCE_TYPE_NAME_FOR_E2E"), + } + + var err error + c.template, err = newPauseCFNTemplate(c.clusterTmplObj) + utility.FailNowIfError(t, "Error while reading pause CFN Template: %v", err) + t.Logf("Pause test setup complete. ProjectID: %s, ClusterName: %s", c.clusterTmplObj.ProjectID, c.clusterTmplObj.Name) +} + +func newPauseCFNTemplate(tmpl pauseTestCluster) (string, error) { + return utility.ExecuteGoTemplate(pauseCfnTemplatePath, tmpl) +} + +func testCreatePauseStack(t *testing.T, c *pauseTestContext) { + t.Helper() + t.Logf("Creating pause stack with template:\n%s", c.template) + + output := utility.CreateStack(t, c.cfnClient, pauseStackName, c.template) + clusterID := getPauseClusterIDFromStack(output) + + cluster := readPauseClusterFromAtlas(t, c) + + assert.Equal(t, cluster.GetId(), clusterID) + assert.False(t, cluster.GetPaused()) +} + +func testUpdatePauseState(t *testing.T, c *pauseTestContext, pause bool) { + t.Helper() + + if pause { + c.clusterTmplObj.Paused = "true" + } else { + c.clusterTmplObj.Paused = "false" + } + + var err error + c.template, err = newPauseCFNTemplate(c.clusterTmplObj) + utility.FailNowIfError(t, "Error while reading pause CFN Template: %v", err) + + output := utility.UpdateStack(t, c.cfnClient, pauseStackName, c.template) + _ = getPauseClusterIDFromStack(output) + + cluster := readPauseClusterFromAtlas(t, c) + + assert.Equal(t, pause, cluster.GetPaused()) +} + +func testDeletePauseStack(t *testing.T, c *pauseTestContext) { + t.Helper() + utility.DeleteStack(t, c.cfnClient, pauseStackName) + _, resp, _ := c.atlasClient20231115014.ClustersApi.GetCluster(ctx.Background(), c.clusterTmplObj.ProjectID, c.clusterTmplObj.Name).Execute() + assert.Equal(t, 404, resp.StatusCode) +} + +func cleanupPauseResources(t *testing.T, c *pauseTestContext) { + t.Helper() + utility.DeleteStackForCleanup(t, c.cfnClient, pauseStackName) +} + +func cleanupPausePrerequisites(t *testing.T, c *pauseTestContext) { + t.Helper() + t.Log("Cleaning up pause test prerequisites") + if os.Getenv("MONGODB_ATLAS_PROJECT_ID") == "" { + utility.DeleteProject(t, c.atlasClient20231115014, c.clusterTmplObj.ProjectID) + } else { + t.Log("skipping project deletion (project managed outside of test)") + } +} + +func readPauseClusterFromAtlas(t *testing.T, c *pauseTestContext) *admin20231115014.AdvancedClusterDescription { + t.Helper() + context := ctx.Background() + projectID := c.clusterTmplObj.ProjectID + cluster, resp, err := c.atlasClient20231115014.ClustersApi.GetCluster(context, projectID, c.clusterTmplObj.Name).Execute() + utility.FailNowIfError(t, "Err while retrieving Cluster from Atlas: %v", err) + assert.Equal(t, 200, resp.StatusCode) + return cluster +} + +func getPauseClusterIDFromStack(output *cloudformation.DescribeStacksOutput) string { + stackOutputs := output.Stacks[0].Outputs + for i := 0; i < len(stackOutputs); i++ { + if *stackOutputs[i].OutputKey == "MongoDBAtlasClusterID" { + return *stackOutputs[i].OutputValue + } + } + return "" +}