Skip to content

Commit

Permalink
CLOUDP-170531: Fix backup auto-export (#923)
Browse files Browse the repository at this point in the history
  • Loading branch information
helderjs authored Apr 13, 2023
1 parent 237e98e commit c949067
Show file tree
Hide file tree
Showing 9 changed files with 631 additions and 7 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ jobs:
"x509auth",
"custom-roles",
"teams",
"backup-config"
]
steps:
- name: Get repo files from cache
Expand Down
8 changes: 4 additions & 4 deletions config/crd/bases/atlas.mongodb.com_atlasbackupschedules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ spec:
snapshots to AWS bucket.
properties:
exportBucketId:
description: Unique identifier of the AWS bucket to export the
cloud backup snapshot to.
description: Unique Atlas identifier of the AWS bucket which was
granted access to export backup snapshot
type: string
frequencyType:
default: MONTHLY
default: monthly
enum:
- MONTHLY
- monthly
type: string
required:
- exportBucketId
Expand Down
6 changes: 3 additions & 3 deletions pkg/api/v1/atlasbackupschedule_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ type AtlasBackupScheduleSpec struct {
}

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

Expand Down
49 changes: 49 additions & 0 deletions test/e2e/actions/project_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ import (
"context"
"fmt"
"path"
"time"

"k8s.io/apimachinery/pkg/types"

mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1"

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status"
"github.com/mongodb/mongodb-atlas-kubernetes/test/helper"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -56,3 +64,44 @@ func CreateNamespaceAndSecrets(userData *model.TestDataProvider) {
CreateConnectionAtlasKey(userData)
}
}

func CreateProjectWithCloudProviderAccess(testData *model.TestDataProvider, atlasIAMRoleName string) {
ProjectCreationFlow(testData)

By("Configure cloud provider access", func() {
testData.Project.Spec.CloudProviderAccessRoles = []mdbv1.CloudProviderAccessRole{
{
ProviderName: "AWS",
},
}
Expect(testData.K8SClient.Update(testData.Context, testData.Project)).To(Succeed())

Eventually(func(g Gomega) bool {
g.Expect(testData.K8SClient.Get(testData.Context, types.NamespacedName{
Name: testData.Project.Name,
Namespace: testData.Project.Namespace,
}, testData.Project)).To(Succeed())

g.Expect(testData.Project.Status.CloudProviderAccessRoles).ShouldNot(BeEmpty())

return testData.Project.Status.CloudProviderAccessRoles[0].Status == status.StatusEmptyARN
}).WithTimeout(5 * time.Minute).WithPolling(10 * time.Second).Should(BeTrue())

roleArn, err := testData.AWSResourcesGenerator.CreateIAMRole(atlasIAMRoleName, func() helper.IAMPolicy {
cloudProviderAccess := testData.Project.Status.CloudProviderAccessRoles[0]
return helper.CloudProviderAccessPolicy(cloudProviderAccess.AtlasAWSAccountArn, cloudProviderAccess.AtlasAssumedRoleExternalID)
})

Expect(err).Should(BeNil())
Expect(roleArn).ShouldNot(BeEmpty())

testData.AWSResourcesGenerator.Cleanup(func() {
Expect(testData.AWSResourcesGenerator.DeleteIAMRole(atlasIAMRoleName)).To(Succeed())
})

testData.Project.Spec.CloudProviderAccessRoles[0].IamAssumedRoleArn = roleArn
Expect(testData.K8SClient.Update(testData.Context, testData.Project)).To(Succeed())

WaitForConditionsToBecomeTrue(testData, status.CloudProviderAccessReadyType, status.ReadyType)
})
}
17 changes: 17 additions & 0 deletions test/e2e/api/atlas/atlas.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,20 @@ func (a *Atlas) GetOrgUsers(projectID string) ([]mongodbatlas.AtlasUser, error)

return users, nil
}

func (a *Atlas) CreateExportBucket(projectID, bucketName, roleID string) (*mongodbatlas.CloudProviderSnapshotExportBucket, error) {
r, _, err := a.Client.CloudProviderSnapshotExportBuckets.Create(
context.Background(),
projectID,
&mongodbatlas.CloudProviderSnapshotExportBucket{
BucketName: bucketName,
CloudProvider: "AWS",
IAMRoleID: roleID,
},
)
if err != nil {
return nil, err
}

return r, nil
}
228 changes: 228 additions & 0 deletions test/e2e/backup_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package e2e_test

import (
"fmt"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/common"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr"
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/actions"
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/actions/deploy"
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/api/atlas"
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/data"
"github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/model"
"github.com/mongodb/mongodb-atlas-kubernetes/test/helper"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1"
)

const (
atlasIAMRoleName = "atlas-role"
atlasBucketPolicyName = "atlas-bucket-export-policy"
bucketName = "cloud-backup-snapshot"
)

var _ = FDescribe("Deployment Backup Configuration", Label("backup-config"), func() {
var testData *model.TestDataProvider

AfterEach(func() {
GinkgoWriter.Write([]byte("\n"))
GinkgoWriter.Write([]byte("===============================================\n"))
GinkgoWriter.Write([]byte("Operator namespace: " + testData.Resources.Namespace + "\n"))
GinkgoWriter.Write([]byte("===============================================\n"))
if CurrentSpecReport().Failed() {
Expect(actions.SaveProjectsToFile(testData.Context, testData.K8SClient, testData.Resources.Namespace)).Should(Succeed())
Expect(actions.SaveDeploymentsToFile(testData.Context, testData.K8SClient, testData.Resources.Namespace)).Should(Succeed())
Expect(actions.SaveUsersToFile(testData.Context, testData.K8SClient, testData.Resources.Namespace)).Should(Succeed())
}

By("Should clean up created resources", func() {
actions.DeleteTestDataDeployments(testData)
actions.DeleteTestDataProject(testData)

actions.AfterEachFinalCleanup([]model.TestDataProvider{*testData})
})
})

DescribeTable("Configure backup for a deployment",
func(test *model.TestDataProvider) {
testData = test

bucket := fmt.Sprintf("%s-%s", bucketName, testData.Resources.TestID)
bucketPolicy := fmt.Sprintf("%s-%s", atlasBucketPolicyName, testData.Resources.TestID)
role := fmt.Sprintf("%s-%s", atlasIAMRoleName, testData.Resources.TestID)

actions.CreateProjectWithCloudProviderAccess(testData, role)
setupAWSResource(testData.AWSResourcesGenerator, bucket, bucketPolicy, role)
deploy.CreateInitialDeployments(testData)

backupConfigFlow(test, bucket)
},
Entry(
"Enable backup for a deployment",
model.DataProvider(
"deployment-backup-enabled",
model.NewEmptyAtlasKeyType().UseDefaultFullAccess(),
30001,
[]func(*model.TestDataProvider){},
).
WithProject(data.DefaultProject()).
WithInitialDeployments(data.CreateAdvancedDeployment("backup-deployment")),
),
)
})

func backupConfigFlow(data *model.TestDataProvider, bucket string) {
By("Enable backup for deployment", func() {
Expect(data.K8SClient.Get(data.Context, client.ObjectKeyFromObject(data.InitialDeployments[0]), data.InitialDeployments[0])).To(Succeed())
data.InitialDeployments[0].Spec.AdvancedDeploymentSpec.BackupEnabled = toptr.MakePtr(true)
Expect(data.K8SClient.Update(data.Context, data.InitialDeployments[0])).To(Succeed())

Eventually(func(g Gomega) bool {
objectKey := types.NamespacedName{
Name: data.InitialDeployments[0].Name,
Namespace: data.InitialDeployments[0].Namespace,
}
g.Expect(data.K8SClient.Get(data.Context, objectKey, data.InitialDeployments[0])).To(Succeed())
return data.InitialDeployments[0].Status.StateName == status.StateIDLE
}).WithTimeout(30 * time.Minute).Should(BeTrue())
})

By("Configure backup schedule and policy for the deployment", func() {
bkpPolicy := &mdbv1.AtlasBackupPolicy{
ObjectMeta: metav1.ObjectMeta{
Namespace: data.Project.Namespace,
Name: fmt.Sprintf("%s-bkp-policy", data.Project.Name),
},
Spec: mdbv1.AtlasBackupPolicySpec{
Items: []mdbv1.AtlasBackupPolicyItem{
{
FrequencyInterval: 6,
FrequencyType: "hourly",
RetentionValue: 2,
RetentionUnit: "days",
},
{
FrequencyInterval: 1,
FrequencyType: "daily",
RetentionValue: 7,
RetentionUnit: "days",
},
{
FrequencyInterval: 1,
FrequencyType: "weekly",
RetentionValue: 4,
RetentionUnit: "weeks",
},
{
FrequencyInterval: 1,
FrequencyType: "monthly",
RetentionValue: 12,
RetentionUnit: "months",
},
},
},
}
Expect(data.K8SClient.Create(data.Context, bkpPolicy)).To(Succeed())

bkpSchedule := &mdbv1.AtlasBackupSchedule{
ObjectMeta: metav1.ObjectMeta{
Namespace: data.Project.Namespace,
Name: fmt.Sprintf("%s-bkp-schedule", data.Project.Name),
},
Spec: mdbv1.AtlasBackupScheduleSpec{
PolicyRef: common.ResourceRefNamespaced{
Namespace: data.Project.Namespace,
Name: fmt.Sprintf("%s-bkp-policy", data.Project.Name),
},
ReferenceHourOfDay: 19,
ReferenceMinuteOfHour: 2,
RestoreWindowDays: 1,
UseOrgAndGroupNamesInExportPrefix: true,
},
}
Expect(data.K8SClient.Create(data.Context, bkpSchedule)).To(Succeed())

Expect(data.K8SClient.Get(data.Context, client.ObjectKeyFromObject(data.InitialDeployments[0]), data.InitialDeployments[0])).To(Succeed())
data.InitialDeployments[0].Spec.BackupScheduleRef = common.ResourceRefNamespaced{
Namespace: data.Project.Namespace,
Name: fmt.Sprintf("%s-bkp-schedule", data.Project.Name),
}
Expect(data.K8SClient.Update(data.Context, data.InitialDeployments[0])).To(Succeed())

Eventually(func(g Gomega) bool {
g.Expect(data.K8SClient.Get(data.Context, types.NamespacedName{
Name: data.InitialDeployments[0].Name,
Namespace: data.InitialDeployments[0].Namespace,
}, data.InitialDeployments[0])).To(Succeed())

return data.InitialDeployments[0].Status.StateName == status.StateIDLE
}).WithTimeout(30 * time.Minute).Should(BeTrue())
})

By("Configure auto export to AWS bucket", func() {
aClient := atlas.GetClientOrFail()
exportBucket, err := aClient.CreateExportBucket(
data.Project.ID(),
bucket,
data.Project.Status.CloudProviderAccessRoles[0].RoleID,
)
Expect(err).Should(BeNil())
Expect(exportBucket).ShouldNot(BeNil())

backupSchedule := &mdbv1.AtlasBackupSchedule{
ObjectMeta: metav1.ObjectMeta{
Namespace: data.Project.Namespace,
Name: fmt.Sprintf("%s-bkp-schedule", data.Project.Name),
},
}
Expect(data.K8SClient.Get(data.Context, client.ObjectKeyFromObject(backupSchedule), backupSchedule)).To(Succeed())

backupSchedule.Spec.AutoExportEnabled = true
backupSchedule.Spec.Export = &mdbv1.AtlasBackupExportSpec{
ExportBucketID: exportBucket.ID,
FrequencyType: "monthly",
}
Expect(data.K8SClient.Update(data.Context, backupSchedule)).To(Succeed())

Eventually(func(g Gomega) bool {
g.Expect(data.K8SClient.Get(data.Context, types.NamespacedName{
Name: data.InitialDeployments[0].Name,
Namespace: data.InitialDeployments[0].Namespace,
}, data.InitialDeployments[0])).To(Succeed())

return data.InitialDeployments[0].Status.StateName == status.StateIDLE
}).WithTimeout(30 * time.Minute).Should(BeTrue())
})
}

func setupAWSResource(gen *helper.AwsResourcesGenerator, bucket, bucketPolicy, role string) {
Expect(gen.CreateBucket(bucket)).To(Succeed())
gen.Cleanup(func() {
Expect(gen.EmptyBucket(bucket)).To(Succeed())
Expect(gen.DeleteBucket(bucket)).To(Succeed())
})

policyArn, err := gen.CreatePolicy(bucketPolicy, func() helper.IAMPolicy {
return helper.BucketExportPolicy(bucket)
})
Expect(err).Should(BeNil())
Expect(policyArn).ShouldNot(BeEmpty())
gen.Cleanup(func() {
Expect(gen.DeletePolicy(policyArn)).To(Succeed())
})

Expect(gen.AttachRolePolicy(role, policyArn)).To(Succeed())
gen.Cleanup(func() {
Expect(gen.DetachRolePolicy(role, policyArn)).To(Succeed())
})
}
Loading

0 comments on commit c949067

Please sign in to comment.