Skip to content

Commit c949067

Browse files
authored
CLOUDP-170531: Fix backup auto-export (#923)
1 parent 237e98e commit c949067

File tree

9 files changed

+631
-7
lines changed

9 files changed

+631
-7
lines changed

.github/workflows/test-e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ jobs:
152152
"x509auth",
153153
"custom-roles",
154154
"teams",
155+
"backup-config"
155156
]
156157
steps:
157158
- name: Get repo files from cache

config/crd/bases/atlas.mongodb.com_atlasbackupschedules.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,13 @@ spec:
8282
snapshots to AWS bucket.
8383
properties:
8484
exportBucketId:
85-
description: Unique identifier of the AWS bucket to export the
86-
cloud backup snapshot to.
85+
description: Unique Atlas identifier of the AWS bucket which was
86+
granted access to export backup snapshot
8787
type: string
8888
frequencyType:
89-
default: MONTHLY
89+
default: monthly
9090
enum:
91-
- MONTHLY
91+
- monthly
9292
type: string
9393
required:
9494
- exportBucketId

pkg/api/v1/atlasbackupschedule_types.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ type AtlasBackupScheduleSpec struct {
6161
}
6262

6363
type AtlasBackupExportSpec struct {
64-
// Unique identifier of the AWS bucket to export the cloud backup snapshot to.
64+
// Unique Atlas identifier of the AWS bucket which was granted access to export backup snapshot
6565
ExportBucketID string `json:"exportBucketId"`
66-
// +kubebuilder:validation:Enum:=MONTHLY
67-
// +kubebuilder:default:=MONTHLY
66+
// +kubebuilder:validation:Enum:=monthly
67+
// +kubebuilder:default:=monthly
6868
FrequencyType string `json:"frequencyType"`
6969
}
7070

test/e2e/actions/project_flow.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import (
44
"context"
55
"fmt"
66
"path"
7+
"time"
8+
9+
"k8s.io/apimachinery/pkg/types"
10+
11+
mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1"
12+
13+
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status"
14+
"github.com/mongodb/mongodb-atlas-kubernetes/test/helper"
715

816
. "github.com/onsi/ginkgo/v2"
917
. "github.com/onsi/gomega"
@@ -56,3 +64,44 @@ func CreateNamespaceAndSecrets(userData *model.TestDataProvider) {
5664
CreateConnectionAtlasKey(userData)
5765
}
5866
}
67+
68+
func CreateProjectWithCloudProviderAccess(testData *model.TestDataProvider, atlasIAMRoleName string) {
69+
ProjectCreationFlow(testData)
70+
71+
By("Configure cloud provider access", func() {
72+
testData.Project.Spec.CloudProviderAccessRoles = []mdbv1.CloudProviderAccessRole{
73+
{
74+
ProviderName: "AWS",
75+
},
76+
}
77+
Expect(testData.K8SClient.Update(testData.Context, testData.Project)).To(Succeed())
78+
79+
Eventually(func(g Gomega) bool {
80+
g.Expect(testData.K8SClient.Get(testData.Context, types.NamespacedName{
81+
Name: testData.Project.Name,
82+
Namespace: testData.Project.Namespace,
83+
}, testData.Project)).To(Succeed())
84+
85+
g.Expect(testData.Project.Status.CloudProviderAccessRoles).ShouldNot(BeEmpty())
86+
87+
return testData.Project.Status.CloudProviderAccessRoles[0].Status == status.StatusEmptyARN
88+
}).WithTimeout(5 * time.Minute).WithPolling(10 * time.Second).Should(BeTrue())
89+
90+
roleArn, err := testData.AWSResourcesGenerator.CreateIAMRole(atlasIAMRoleName, func() helper.IAMPolicy {
91+
cloudProviderAccess := testData.Project.Status.CloudProviderAccessRoles[0]
92+
return helper.CloudProviderAccessPolicy(cloudProviderAccess.AtlasAWSAccountArn, cloudProviderAccess.AtlasAssumedRoleExternalID)
93+
})
94+
95+
Expect(err).Should(BeNil())
96+
Expect(roleArn).ShouldNot(BeEmpty())
97+
98+
testData.AWSResourcesGenerator.Cleanup(func() {
99+
Expect(testData.AWSResourcesGenerator.DeleteIAMRole(atlasIAMRoleName)).To(Succeed())
100+
})
101+
102+
testData.Project.Spec.CloudProviderAccessRoles[0].IamAssumedRoleArn = roleArn
103+
Expect(testData.K8SClient.Update(testData.Context, testData.Project)).To(Succeed())
104+
105+
WaitForConditionsToBecomeTrue(testData, status.CloudProviderAccessReadyType, status.ReadyType)
106+
})
107+
}

test/e2e/api/atlas/atlas.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,20 @@ func (a *Atlas) GetOrgUsers(projectID string) ([]mongodbatlas.AtlasUser, error)
206206

207207
return users, nil
208208
}
209+
210+
func (a *Atlas) CreateExportBucket(projectID, bucketName, roleID string) (*mongodbatlas.CloudProviderSnapshotExportBucket, error) {
211+
r, _, err := a.Client.CloudProviderSnapshotExportBuckets.Create(
212+
context.Background(),
213+
projectID,
214+
&mongodbatlas.CloudProviderSnapshotExportBucket{
215+
BucketName: bucketName,
216+
CloudProvider: "AWS",
217+
IAMRoleID: roleID,
218+
},
219+
)
220+
if err != nil {
221+
return nil, err
222+
}
223+
224+
return r, nil
225+
}

test/e2e/backup_config_test.go

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package e2e_test
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
10+
"k8s.io/apimachinery/pkg/types"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
12+
13+
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/common"
14+
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status"
15+
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr"
16+
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/actions"
17+
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/actions/deploy"
18+
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/api/atlas"
19+
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/data"
20+
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/model"
21+
"github.com/mongodb/mongodb-atlas-kubernetes/test/helper"
22+
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
25+
mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1"
26+
)
27+
28+
const (
29+
atlasIAMRoleName = "atlas-role"
30+
atlasBucketPolicyName = "atlas-bucket-export-policy"
31+
bucketName = "cloud-backup-snapshot"
32+
)
33+
34+
var _ = FDescribe("Deployment Backup Configuration", Label("backup-config"), func() {
35+
var testData *model.TestDataProvider
36+
37+
AfterEach(func() {
38+
GinkgoWriter.Write([]byte("\n"))
39+
GinkgoWriter.Write([]byte("===============================================\n"))
40+
GinkgoWriter.Write([]byte("Operator namespace: " + testData.Resources.Namespace + "\n"))
41+
GinkgoWriter.Write([]byte("===============================================\n"))
42+
if CurrentSpecReport().Failed() {
43+
Expect(actions.SaveProjectsToFile(testData.Context, testData.K8SClient, testData.Resources.Namespace)).Should(Succeed())
44+
Expect(actions.SaveDeploymentsToFile(testData.Context, testData.K8SClient, testData.Resources.Namespace)).Should(Succeed())
45+
Expect(actions.SaveUsersToFile(testData.Context, testData.K8SClient, testData.Resources.Namespace)).Should(Succeed())
46+
}
47+
48+
By("Should clean up created resources", func() {
49+
actions.DeleteTestDataDeployments(testData)
50+
actions.DeleteTestDataProject(testData)
51+
52+
actions.AfterEachFinalCleanup([]model.TestDataProvider{*testData})
53+
})
54+
})
55+
56+
DescribeTable("Configure backup for a deployment",
57+
func(test *model.TestDataProvider) {
58+
testData = test
59+
60+
bucket := fmt.Sprintf("%s-%s", bucketName, testData.Resources.TestID)
61+
bucketPolicy := fmt.Sprintf("%s-%s", atlasBucketPolicyName, testData.Resources.TestID)
62+
role := fmt.Sprintf("%s-%s", atlasIAMRoleName, testData.Resources.TestID)
63+
64+
actions.CreateProjectWithCloudProviderAccess(testData, role)
65+
setupAWSResource(testData.AWSResourcesGenerator, bucket, bucketPolicy, role)
66+
deploy.CreateInitialDeployments(testData)
67+
68+
backupConfigFlow(test, bucket)
69+
},
70+
Entry(
71+
"Enable backup for a deployment",
72+
model.DataProvider(
73+
"deployment-backup-enabled",
74+
model.NewEmptyAtlasKeyType().UseDefaultFullAccess(),
75+
30001,
76+
[]func(*model.TestDataProvider){},
77+
).
78+
WithProject(data.DefaultProject()).
79+
WithInitialDeployments(data.CreateAdvancedDeployment("backup-deployment")),
80+
),
81+
)
82+
})
83+
84+
func backupConfigFlow(data *model.TestDataProvider, bucket string) {
85+
By("Enable backup for deployment", func() {
86+
Expect(data.K8SClient.Get(data.Context, client.ObjectKeyFromObject(data.InitialDeployments[0]), data.InitialDeployments[0])).To(Succeed())
87+
data.InitialDeployments[0].Spec.AdvancedDeploymentSpec.BackupEnabled = toptr.MakePtr(true)
88+
Expect(data.K8SClient.Update(data.Context, data.InitialDeployments[0])).To(Succeed())
89+
90+
Eventually(func(g Gomega) bool {
91+
objectKey := types.NamespacedName{
92+
Name: data.InitialDeployments[0].Name,
93+
Namespace: data.InitialDeployments[0].Namespace,
94+
}
95+
g.Expect(data.K8SClient.Get(data.Context, objectKey, data.InitialDeployments[0])).To(Succeed())
96+
return data.InitialDeployments[0].Status.StateName == status.StateIDLE
97+
}).WithTimeout(30 * time.Minute).Should(BeTrue())
98+
})
99+
100+
By("Configure backup schedule and policy for the deployment", func() {
101+
bkpPolicy := &mdbv1.AtlasBackupPolicy{
102+
ObjectMeta: metav1.ObjectMeta{
103+
Namespace: data.Project.Namespace,
104+
Name: fmt.Sprintf("%s-bkp-policy", data.Project.Name),
105+
},
106+
Spec: mdbv1.AtlasBackupPolicySpec{
107+
Items: []mdbv1.AtlasBackupPolicyItem{
108+
{
109+
FrequencyInterval: 6,
110+
FrequencyType: "hourly",
111+
RetentionValue: 2,
112+
RetentionUnit: "days",
113+
},
114+
{
115+
FrequencyInterval: 1,
116+
FrequencyType: "daily",
117+
RetentionValue: 7,
118+
RetentionUnit: "days",
119+
},
120+
{
121+
FrequencyInterval: 1,
122+
FrequencyType: "weekly",
123+
RetentionValue: 4,
124+
RetentionUnit: "weeks",
125+
},
126+
{
127+
FrequencyInterval: 1,
128+
FrequencyType: "monthly",
129+
RetentionValue: 12,
130+
RetentionUnit: "months",
131+
},
132+
},
133+
},
134+
}
135+
Expect(data.K8SClient.Create(data.Context, bkpPolicy)).To(Succeed())
136+
137+
bkpSchedule := &mdbv1.AtlasBackupSchedule{
138+
ObjectMeta: metav1.ObjectMeta{
139+
Namespace: data.Project.Namespace,
140+
Name: fmt.Sprintf("%s-bkp-schedule", data.Project.Name),
141+
},
142+
Spec: mdbv1.AtlasBackupScheduleSpec{
143+
PolicyRef: common.ResourceRefNamespaced{
144+
Namespace: data.Project.Namespace,
145+
Name: fmt.Sprintf("%s-bkp-policy", data.Project.Name),
146+
},
147+
ReferenceHourOfDay: 19,
148+
ReferenceMinuteOfHour: 2,
149+
RestoreWindowDays: 1,
150+
UseOrgAndGroupNamesInExportPrefix: true,
151+
},
152+
}
153+
Expect(data.K8SClient.Create(data.Context, bkpSchedule)).To(Succeed())
154+
155+
Expect(data.K8SClient.Get(data.Context, client.ObjectKeyFromObject(data.InitialDeployments[0]), data.InitialDeployments[0])).To(Succeed())
156+
data.InitialDeployments[0].Spec.BackupScheduleRef = common.ResourceRefNamespaced{
157+
Namespace: data.Project.Namespace,
158+
Name: fmt.Sprintf("%s-bkp-schedule", data.Project.Name),
159+
}
160+
Expect(data.K8SClient.Update(data.Context, data.InitialDeployments[0])).To(Succeed())
161+
162+
Eventually(func(g Gomega) bool {
163+
g.Expect(data.K8SClient.Get(data.Context, types.NamespacedName{
164+
Name: data.InitialDeployments[0].Name,
165+
Namespace: data.InitialDeployments[0].Namespace,
166+
}, data.InitialDeployments[0])).To(Succeed())
167+
168+
return data.InitialDeployments[0].Status.StateName == status.StateIDLE
169+
}).WithTimeout(30 * time.Minute).Should(BeTrue())
170+
})
171+
172+
By("Configure auto export to AWS bucket", func() {
173+
aClient := atlas.GetClientOrFail()
174+
exportBucket, err := aClient.CreateExportBucket(
175+
data.Project.ID(),
176+
bucket,
177+
data.Project.Status.CloudProviderAccessRoles[0].RoleID,
178+
)
179+
Expect(err).Should(BeNil())
180+
Expect(exportBucket).ShouldNot(BeNil())
181+
182+
backupSchedule := &mdbv1.AtlasBackupSchedule{
183+
ObjectMeta: metav1.ObjectMeta{
184+
Namespace: data.Project.Namespace,
185+
Name: fmt.Sprintf("%s-bkp-schedule", data.Project.Name),
186+
},
187+
}
188+
Expect(data.K8SClient.Get(data.Context, client.ObjectKeyFromObject(backupSchedule), backupSchedule)).To(Succeed())
189+
190+
backupSchedule.Spec.AutoExportEnabled = true
191+
backupSchedule.Spec.Export = &mdbv1.AtlasBackupExportSpec{
192+
ExportBucketID: exportBucket.ID,
193+
FrequencyType: "monthly",
194+
}
195+
Expect(data.K8SClient.Update(data.Context, backupSchedule)).To(Succeed())
196+
197+
Eventually(func(g Gomega) bool {
198+
g.Expect(data.K8SClient.Get(data.Context, types.NamespacedName{
199+
Name: data.InitialDeployments[0].Name,
200+
Namespace: data.InitialDeployments[0].Namespace,
201+
}, data.InitialDeployments[0])).To(Succeed())
202+
203+
return data.InitialDeployments[0].Status.StateName == status.StateIDLE
204+
}).WithTimeout(30 * time.Minute).Should(BeTrue())
205+
})
206+
}
207+
208+
func setupAWSResource(gen *helper.AwsResourcesGenerator, bucket, bucketPolicy, role string) {
209+
Expect(gen.CreateBucket(bucket)).To(Succeed())
210+
gen.Cleanup(func() {
211+
Expect(gen.EmptyBucket(bucket)).To(Succeed())
212+
Expect(gen.DeleteBucket(bucket)).To(Succeed())
213+
})
214+
215+
policyArn, err := gen.CreatePolicy(bucketPolicy, func() helper.IAMPolicy {
216+
return helper.BucketExportPolicy(bucket)
217+
})
218+
Expect(err).Should(BeNil())
219+
Expect(policyArn).ShouldNot(BeEmpty())
220+
gen.Cleanup(func() {
221+
Expect(gen.DeletePolicy(policyArn)).To(Succeed())
222+
})
223+
224+
Expect(gen.AttachRolePolicy(role, policyArn)).To(Succeed())
225+
gen.Cleanup(func() {
226+
Expect(gen.DetachRolePolicy(role, policyArn)).To(Succeed())
227+
})
228+
}

0 commit comments

Comments
 (0)