Skip to content

Commit 4f7a75b

Browse files
fix: Avoid error in unpause operation of Cluster resource (#1469)
1 parent f9ef12b commit 4f7a75b

File tree

4 files changed

+304
-2
lines changed

4 files changed

+304
-2
lines changed

.github/workflows/e2e-testing.yaml

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,41 @@ jobs:
6363
MONGODB_ATLAS_SECRET_PROFILE: cfn-cloud-dev-github-action
6464
run: |
6565
cd cfn-resources/test/e2e/cluster
66-
go test -timeout 90m -v cluster_test.go
66+
go test -timeout 90m -v -run '^TestClusterCFN$' .
67+
68+
# Run idividual test in separate test group for parallel execution.
69+
# Due to usage of t.Setenv() in test code t.Parallel() is not possible.
70+
# Having both tests run in same `go test` execution is not possible due to scripts used for private registry publishing modifying same files
71+
cluster-pause:
72+
needs: change-detection
73+
if: ${{ needs.change-detection.outputs.cluster == 'true' }}
74+
runs-on: ubuntu-latest
75+
steps:
76+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
77+
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c
78+
with:
79+
python-version: '3.9'
80+
cache: 'pip'
81+
- run: pip install cloudformation-cli-go-plugin
82+
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00
83+
with:
84+
go-version-file: 'cfn-resources/go.mod'
85+
- uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8
86+
with:
87+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_TEST_ENV }}
88+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_TEST_ENV }}
89+
aws-region: eu-west-1
90+
- name: Run E2E test
91+
shell: bash
92+
env:
93+
MONGODB_ATLAS_PUBLIC_KEY: ${{ secrets.CLOUD_DEV_PUBLIC_KEY }}
94+
MONGODB_ATLAS_PRIVATE_KEY: ${{ secrets.CLOUD_DEV_PRIVATE_KEY }}
95+
MONGODB_ATLAS_ORG_ID: ${{ secrets.CLOUD_DEV_ORG_ID }}
96+
MONGODB_ATLAS_BASE_URL: https://cloud-dev.mongodb.com/
97+
MONGODB_ATLAS_SECRET_PROFILE: cfn-cloud-dev-github-action
98+
run: |
99+
cd cfn-resources/test/e2e/cluster
100+
go test -timeout 90m -v -run '^TestClusterPauseCFN$' .
67101
68102

69103
flex-cluster:

cfn-resources/cluster/cmd/resource/resource.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,19 @@ func Update(req handler.Request, prevModel *Model, currentModel *Model) (handler
120120
return updateClusterCallback(client, currentModel, *currentModel.ProjectId)
121121
}
122122
currentModel.validateDefaultLabel()
123+
124+
currentCluster, resp, err := client.Atlas20231115014.ClustersApi.GetCluster(context.Background(), *currentModel.ProjectId, *currentModel.Name).Execute()
125+
if pe := util.HandleClusterError(err, resp); pe != nil {
126+
return *pe, nil
127+
}
128+
129+
// Unpausing must be handled separately from other updates to avoid errors from the API.
130+
if pe := handleUnpausingUpdate(client, currentCluster, currentModel); pe != nil {
131+
return *pe, nil
132+
}
133+
123134
adminCluster, errEvent := setClusterRequest(currentModel)
124135
if len(adminCluster.GetReplicationSpecs()) > 0 {
125-
currentCluster, _, _ := client.Atlas20231115014.ClustersApi.GetCluster(context.Background(), *currentModel.ProjectId, *currentModel.Name).Execute()
126136
if currentCluster != nil {
127137
adminCluster.ReplicationSpecs = AddReplicationSpecIDs(currentCluster.GetReplicationSpecs(), adminCluster.GetReplicationSpecs())
128138
}
@@ -150,6 +160,15 @@ func Update(req handler.Request, prevModel *Model, currentModel *Model) (handler
150160
return event, nil
151161
}
152162

163+
func handleUnpausingUpdate(client *util.MongoDBClient, currentCluster *admin20231115014.AdvancedClusterDescription, currentModel *Model) *handler.ProgressEvent {
164+
if (currentCluster.Paused != nil && *currentCluster.Paused) && (currentModel.Paused == nil || !*currentModel.Paused) {
165+
_, resp, err := client.Atlas20231115014.ClustersApi.UpdateCluster(context.Background(), *currentModel.ProjectId, *currentModel.Name,
166+
&admin20231115014.AdvancedClusterDescription{Paused: admin20231115014.PtrBool(false)}).Execute()
167+
return util.HandleClusterError(err, resp)
168+
}
169+
return nil
170+
}
171+
153172
// Delete handles the Delete event from the Cloudformation service.
154173
func Delete(req handler.Request, prevModel *Model, currentModel *Model) (handler.ProgressEvent, error) {
155174
client, setupErr := setupRequest(req, currentModel, createReadUpdateDeleteRequiredFields)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"AWSTemplateFormatVersion": "2010-09-09",
3+
"Description": "Minimal template to exercise pausing/unpausing a single REPLICASET cluster",
4+
"Resources": {
5+
"Cluster": {
6+
"Type": "{{ .ResourceTypeName }}",
7+
"Properties": {
8+
"Name": "{{ .Name }}",
9+
"ProjectId": "{{ .ProjectID }}",
10+
"Profile": "{{ .Profile }}",
11+
"ClusterType": "REPLICASET",
12+
"Paused": "{{ .Paused }}",
13+
"ReplicationSpecs": [
14+
{
15+
"NumShards": 1,
16+
"AdvancedRegionConfigs": [
17+
{
18+
"RegionName": "US_EAST_1",
19+
"Priority": 7,
20+
"ProviderName": "AWS",
21+
"ElectableSpecs": {
22+
"EbsVolumeType": "STANDARD",
23+
"InstanceSize": "M10",
24+
"NodeCount": 3
25+
}
26+
}
27+
]
28+
}
29+
]
30+
}
31+
}
32+
},
33+
"Outputs": {
34+
"MongoDBAtlasClusterID": {
35+
"Description": "Cluster Id",
36+
"Value": { "Fn::GetAtt": ["Cluster", "Id"] }
37+
}
38+
}
39+
}
40+
41+
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// Copyright 2024 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package cluster_test
15+
16+
import (
17+
ctx "context"
18+
"os"
19+
"testing"
20+
21+
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
22+
"github.com/mongodb/mongodbatlas-cloudformation-resources/test/e2e/utility"
23+
"github.com/stretchr/testify/assert"
24+
admin20231115014 "go.mongodb.org/atlas-sdk/v20231115014/admin"
25+
)
26+
27+
type pauseTestContext struct {
28+
cfnClient *cloudformation.Client
29+
atlasClient20231115014 *admin20231115014.APIClient
30+
resourceCtx utility.ResourceContext
31+
template string
32+
clusterTmplObj pauseTestCluster
33+
}
34+
35+
type pauseTestCluster struct {
36+
ResourceTypeName string
37+
Name string
38+
Profile string
39+
ProjectID string
40+
Paused string
41+
}
42+
43+
const (
44+
pauseCfnTemplatePath = "cluster_pause.json.template"
45+
)
46+
47+
// Replication specs are hardcoded in the CFN template for this test.
48+
49+
var (
50+
pauseProfile = os.Getenv("MONGODB_ATLAS_SECRET_PROFILE")
51+
pauseOrgID = os.Getenv("MONGODB_ATLAS_ORG_ID")
52+
pauseRandSuffix = utility.GetRandNum().String()
53+
pauseProjectName = "cfn-e2e-cluster-pause" + pauseRandSuffix
54+
pauseClusterName = "cfn-e2e-cluster-pause" + pauseRandSuffix
55+
pauseStackName = "stack-cluster-pause-e2e-" + pauseRandSuffix
56+
)
57+
58+
func TestClusterPauseCFN(t *testing.T) {
59+
testCtx := setupPauseSuite(t)
60+
61+
t.Run("Validate Template", func(t *testing.T) {
62+
utility.TestIsTemplateValid(t, testCtx.cfnClient, testCtx.template)
63+
})
64+
65+
t.Run("Create Stack", func(t *testing.T) {
66+
testCreatePauseStack(t, testCtx)
67+
})
68+
69+
t.Run("Pause Cluster", func(t *testing.T) {
70+
testUpdatePauseState(t, testCtx, true)
71+
})
72+
73+
t.Run("Unpause Cluster", func(t *testing.T) {
74+
testUpdatePauseState(t, testCtx, false)
75+
})
76+
77+
t.Run("Delete Stack", func(t *testing.T) {
78+
testDeletePauseStack(t, testCtx)
79+
})
80+
}
81+
82+
func setupPauseSuite(t *testing.T) *pauseTestContext {
83+
t.Helper()
84+
t.Log("Setting up pause suite")
85+
testCtx := new(pauseTestContext)
86+
testCtx.setUp(t)
87+
return testCtx
88+
}
89+
90+
func (c *pauseTestContext) setUp(t *testing.T) {
91+
t.Helper()
92+
c.resourceCtx = utility.InitResourceCtx(pauseStackName, pauseRandSuffix, resourceTypeName, resourceDirectory)
93+
c.cfnClient, _ = utility.NewClients(t)
94+
_, c.atlasClient20231115014 = utility.NewClients20231115014(t)
95+
96+
utility.PublishToPrivateRegistry(t, c.resourceCtx)
97+
c.setupPrerequisites(t)
98+
}
99+
100+
func (c *pauseTestContext) setupPrerequisites(t *testing.T) {
101+
t.Helper()
102+
t.Cleanup(func() {
103+
cleanupPausePrerequisites(t, c)
104+
cleanupPauseResources(t, c)
105+
})
106+
t.Log("Setting up prerequisites for pause test")
107+
108+
var projectID string
109+
if projectIDEnvVar := os.Getenv("MONGODB_ATLAS_PROJECT_ID"); projectIDEnvVar != "" {
110+
t.Logf("using projectID from env var %s", projectIDEnvVar)
111+
projectID = projectIDEnvVar
112+
} else {
113+
projectID = utility.CreateProject(t, c.atlasClient20231115014, pauseOrgID, pauseProjectName)
114+
}
115+
116+
c.clusterTmplObj = pauseTestCluster{
117+
Name: pauseClusterName,
118+
ProjectID: projectID,
119+
Profile: pauseProfile,
120+
Paused: "false",
121+
ResourceTypeName: os.Getenv("RESOURCE_TYPE_NAME_FOR_E2E"),
122+
}
123+
124+
var err error
125+
c.template, err = newPauseCFNTemplate(c.clusterTmplObj)
126+
utility.FailNowIfError(t, "Error while reading pause CFN Template: %v", err)
127+
t.Logf("Pause test setup complete. ProjectID: %s, ClusterName: %s", c.clusterTmplObj.ProjectID, c.clusterTmplObj.Name)
128+
}
129+
130+
func newPauseCFNTemplate(tmpl pauseTestCluster) (string, error) {
131+
return utility.ExecuteGoTemplate(pauseCfnTemplatePath, tmpl)
132+
}
133+
134+
func testCreatePauseStack(t *testing.T, c *pauseTestContext) {
135+
t.Helper()
136+
t.Logf("Creating pause stack with template:\n%s", c.template)
137+
138+
output := utility.CreateStack(t, c.cfnClient, pauseStackName, c.template)
139+
clusterID := getPauseClusterIDFromStack(output)
140+
141+
cluster := readPauseClusterFromAtlas(t, c)
142+
143+
assert.Equal(t, cluster.GetId(), clusterID)
144+
assert.False(t, cluster.GetPaused())
145+
}
146+
147+
func testUpdatePauseState(t *testing.T, c *pauseTestContext, pause bool) {
148+
t.Helper()
149+
150+
if pause {
151+
c.clusterTmplObj.Paused = "true"
152+
} else {
153+
c.clusterTmplObj.Paused = "false"
154+
}
155+
156+
var err error
157+
c.template, err = newPauseCFNTemplate(c.clusterTmplObj)
158+
utility.FailNowIfError(t, "Error while reading pause CFN Template: %v", err)
159+
160+
output := utility.UpdateStack(t, c.cfnClient, pauseStackName, c.template)
161+
_ = getPauseClusterIDFromStack(output)
162+
163+
cluster := readPauseClusterFromAtlas(t, c)
164+
165+
assert.Equal(t, pause, cluster.GetPaused())
166+
}
167+
168+
func testDeletePauseStack(t *testing.T, c *pauseTestContext) {
169+
t.Helper()
170+
utility.DeleteStack(t, c.cfnClient, pauseStackName)
171+
_, resp, _ := c.atlasClient20231115014.ClustersApi.GetCluster(ctx.Background(), c.clusterTmplObj.ProjectID, c.clusterTmplObj.Name).Execute()
172+
assert.Equal(t, 404, resp.StatusCode)
173+
}
174+
175+
func cleanupPauseResources(t *testing.T, c *pauseTestContext) {
176+
t.Helper()
177+
utility.DeleteStackForCleanup(t, c.cfnClient, pauseStackName)
178+
}
179+
180+
func cleanupPausePrerequisites(t *testing.T, c *pauseTestContext) {
181+
t.Helper()
182+
t.Log("Cleaning up pause test prerequisites")
183+
if os.Getenv("MONGODB_ATLAS_PROJECT_ID") == "" {
184+
utility.DeleteProject(t, c.atlasClient20231115014, c.clusterTmplObj.ProjectID)
185+
} else {
186+
t.Log("skipping project deletion (project managed outside of test)")
187+
}
188+
}
189+
190+
func readPauseClusterFromAtlas(t *testing.T, c *pauseTestContext) *admin20231115014.AdvancedClusterDescription {
191+
t.Helper()
192+
context := ctx.Background()
193+
projectID := c.clusterTmplObj.ProjectID
194+
cluster, resp, err := c.atlasClient20231115014.ClustersApi.GetCluster(context, projectID, c.clusterTmplObj.Name).Execute()
195+
utility.FailNowIfError(t, "Err while retrieving Cluster from Atlas: %v", err)
196+
assert.Equal(t, 200, resp.StatusCode)
197+
return cluster
198+
}
199+
200+
func getPauseClusterIDFromStack(output *cloudformation.DescribeStacksOutput) string {
201+
stackOutputs := output.Stacks[0].Outputs
202+
for i := 0; i < len(stackOutputs); i++ {
203+
if *stackOutputs[i].OutputKey == "MongoDBAtlasClusterID" {
204+
return *stackOutputs[i].OutputValue
205+
}
206+
}
207+
return ""
208+
}

0 commit comments

Comments
 (0)