diff --git a/.github/workflows/contract-testing.yaml b/.github/workflows/contract-testing.yaml index 0ecad68d7..8f1b8c813 100644 --- a/.github/workflows/contract-testing.yaml +++ b/.github/workflows/contract-testing.yaml @@ -16,6 +16,7 @@ jobs: api-key: ${{ steps.filter.outputs.api-key }} auditing: ${{ steps.filter.outputs.auditing }} backup-compliance-policy: ${{ steps.filter.outputs.backup-compliance-policy }} + cloud-backup-snapshot-export-bucket: ${{ steps.filter.outputs.cloud-backup-snapshot-export-bucket }} cloud-backup-restore-jobs: ${{ steps.filter.outputs.cloud-backup-restore-jobs }} cluster-outage-simulation: ${{ steps.filter.outputs.cluster-outage-simulation }} database-user: ${{ steps.filter.outputs.database-user }} @@ -66,6 +67,8 @@ jobs: - 'cfn-resources/auditing/**' backup-compliance-policy: - 'cfn-resources/backup-compliance-policy/**' + cloud-backup-snapshot-export-bucket: + - 'cfn-resources/cloud-backup-snapshot-export-bucket/**' cloud-backup-restore-jobs: - 'cfn-resources/cloud-backup-restore-jobs/**' cluster-outage-simulation: @@ -335,6 +338,47 @@ jobs: make run-contract-testing make delete-test-resources + cloud-backup-snapshot-export-bucket: + needs: change-detection + if: ${{ needs.change-detection.outputs.cloud-backup-snapshot-export-bucket == 'true' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 + with: + go-version-file: 'cfn-resources/go.mod' + - name: setup Atlas CLI + uses: mongodb/atlas-github-action@e3c9e0204659bafbb3b65e1eb1ee745cca0e9f3b + - uses: aws-actions/setup-sam@d78e1a4a9656d3b223e59b80676a797f20093133 + with: + use-installer: true + - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + 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 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: '3.9' + cache: 'pip' # caching pip dependencies + - run: pip install cloudformation-cli cloudformation-cli-go-plugin + - name: Run the Contract test + shell: bash + env: + MONGODB_ATLAS_PUBLIC_API_KEY: ${{ secrets.CLOUD_DEV_PUBLIC_KEY }} + MONGODB_ATLAS_PRIVATE_API_KEY: ${{ secrets.CLOUD_DEV_PRIVATE_KEY }} + MONGODB_ATLAS_ORG_ID: ${{ secrets.CLOUD_DEV_ORG_ID }} + MONGODB_ATLAS_OPS_MANAGER_URL: ${{ vars.MONGODB_ATLAS_BASE_URL }} + MONGODB_ATLAS_PROFILE: cfn-cloud-dev-github-action + run: | + pushd cfn-resources/cloud-backup-snapshot-export-bucket + make create-test-resources + + cat inputs/inputs_1_create.json + + make run-contract-testing + make delete-test-resources + cloud-backup-restore-jobs: needs: change-detection if: ${{ needs.change-detection.outputs.cloud-backup-restore-jobs == 'true' }} diff --git a/cfn-resources/cloud-backup-snapshot-export-bucket/Makefile b/cfn-resources/cloud-backup-snapshot-export-bucket/Makefile index f325497fc..4f3d93ebd 100644 --- a/cfn-resources/cloud-backup-snapshot-export-bucket/Makefile +++ b/cfn-resources/cloud-backup-snapshot-export-bucket/Makefile @@ -1,4 +1,4 @@ -.PHONY: build debug clean +.PHONY: build test clean tags=logging callback metrics scheduler cgo=0 goos=linux @@ -13,7 +13,25 @@ build: debug: cfn generate - env GOOS=$(goos) CGO_ENABLED=$(cgo) GOARCH=$(goarch) go build -ldflags="$(ldXflagsD)" -tags="$(tags)" -o bin/bootstrap cmd/main.go + env GOOS=$(goos) CGO_ENABLED=$(cgo) GOARCH=$(goarch) go build -ldflags="$(ldXflagsD)" -tags="$(tags)" -o bin/debug cmd/main.go clean: rm -rf bin + +submit: clean build # submit to private registry must use release build not debug build + @echo "==> Submitting to private registry for testing" + cfn submit --set-default --region us-east-1 + +create-test-resources: + @echo "==> Creating test files and resources for contract testing" + ./test/contract-testing/cfn-test-create.sh + +delete-test-resources: + @echo "==> Delete test resources used for contract testing" + ./test/contract-testing/cfn-test-delete.sh + +run-contract-testing: + @echo "==> Run contract testing" + make build + sam local start-lambda & + cfn test --function-name TestEntrypoint --verbose diff --git a/cfn-resources/cloud-backup-snapshot-export-bucket/cmd/resource/config.go b/cfn-resources/cloud-backup-snapshot-export-bucket/cmd/resource/config.go new file mode 100644 index 000000000..4d9eb7831 --- /dev/null +++ b/cfn-resources/cloud-backup-snapshot-export-bucket/cmd/resource/config.go @@ -0,0 +1,19 @@ +// Code generated by 'cfn generate', changes will be undone by the next invocation. DO NOT EDIT. +// Updates to this type are made my editing the schema file and executing the 'generate' command. +package resource + +import "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" + +// TypeConfiguration is autogenerated from the json schema +type TypeConfiguration struct { +} + +// Configuration returns a resource's configuration. +func Configuration(req handler.Request) (*TypeConfiguration, error) { + // Populate the type configuration + typeConfig := &TypeConfiguration{} + if err := req.UnmarshalTypeConfig(typeConfig); err != nil { + return typeConfig, err + } + return typeConfig, nil +} diff --git a/cfn-resources/cloud-backup-snapshot-export-bucket/cmd/resource/model.go b/cfn-resources/cloud-backup-snapshot-export-bucket/cmd/resource/model.go index 937e837e2..9458cabf9 100644 --- a/cfn-resources/cloud-backup-snapshot-export-bucket/cmd/resource/model.go +++ b/cfn-resources/cloud-backup-snapshot-export-bucket/cmd/resource/model.go @@ -4,9 +4,10 @@ package resource // Model is autogenerated from the json schema type Model struct { - Profile *string `json:",omitempty"` - BucketName *string `json:",omitempty"` - ProjectId *string `json:",omitempty"` - IamRoleID *string `json:",omitempty"` - Id *string `json:",omitempty"` + Profile *string `json:",omitempty"` + BucketName *string `json:",omitempty"` + ProjectId *string `json:",omitempty"` + IamRoleID *string `json:",omitempty"` + RequirePrivateNetworking *bool `json:",omitempty"` + Id *string `json:",omitempty"` } diff --git a/cfn-resources/cloud-backup-snapshot-export-bucket/cmd/resource/resource.go b/cfn-resources/cloud-backup-snapshot-export-bucket/cmd/resource/resource.go index 5413eb952..6629054ca 100644 --- a/cfn-resources/cloud-backup-snapshot-export-bucket/cmd/resource/resource.go +++ b/cfn-resources/cloud-backup-snapshot-export-bucket/cmd/resource/resource.go @@ -19,12 +19,11 @@ import ( "errors" "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/mongodb/mongodbatlas-cloudformation-resources/util" "github.com/mongodb/mongodbatlas-cloudformation-resources/util/constants" "github.com/mongodb/mongodbatlas-cloudformation-resources/util/progressevent" "github.com/mongodb/mongodbatlas-cloudformation-resources/util/validator" - admin20231115002 "go.mongodb.org/atlas-sdk/v20231115002/admin" + "go.mongodb.org/atlas-sdk/v20250312013/admin" ) const ( @@ -53,17 +52,18 @@ func Create(req handler.Request, prevModel *Model, currentModel *Model) (handler return *pe, nil } - params := &admin20231115002.DiskBackupSnapshotAWSExportBucket{ - BucketName: currentModel.BucketName, - CloudProvider: aws.String(constants.AWS), - IamRoleId: currentModel.IamRoleID, + params := &admin.DiskBackupSnapshotExportBucketRequest{ + BucketName: currentModel.BucketName, + CloudProvider: constants.AWS, + IamRoleId: currentModel.IamRoleID, + RequirePrivateNetworking: currentModel.RequirePrivateNetworking, } - output, resp, err := client.Atlas20231115002.CloudBackupsApi.CreateExportBucket(context.Background(), *currentModel.ProjectId, params).Execute() + output, resp, err := client.AtlasSDK.CloudBackupsApi.CreateExportBucket(context.Background(), *currentModel.ProjectId, params).Execute() if err != nil { return progressevent.GetFailedEventByResponse(err.Error(), resp), nil } - currentModel.Id = output.Id + currentModel.Id = &output.Id return handler.ProgressEvent{ OperationStatus: handler.Success, @@ -84,7 +84,7 @@ func Read(req handler.Request, prevModel *Model, currentModel *Model) (handler.P return *pe, nil } - output, resp, err := client.Atlas20231115002.CloudBackupsApi.GetExportBucket(context.Background(), *currentModel.ProjectId, *currentModel.Id).Execute() + output, resp, err := client.AtlasSDK.CloudBackupsApi.GetExportBucket(context.Background(), *currentModel.ProjectId, *currentModel.Id).Execute() if err != nil { return progressevent.GetFailedEventByResponse(err.Error(), resp), nil } @@ -114,7 +114,7 @@ func Delete(req handler.Request, prevModel *Model, currentModel *Model) (handler return *pe, nil } - _, resp, err := client.Atlas20231115002.CloudBackupsApi.DeleteExportBucket(context.Background(), *currentModel.ProjectId, *currentModel.Id).Execute() + resp, err := client.AtlasSDK.CloudBackupsApi.DeleteExportBucket(context.Background(), *currentModel.ProjectId, *currentModel.Id).Execute() if err != nil { return progressevent.GetFailedEventByResponse(err.Error(), resp), nil } @@ -137,20 +137,22 @@ func List(req handler.Request, prevModel *Model, currentModel *Model) (handler.P return *pe, nil } - output, resp, err := client.Atlas20231115002.CloudBackupsApi.ListExportBuckets(context.Background(), *currentModel.ProjectId).Execute() + output, resp, err := client.AtlasSDK.CloudBackupsApi.ListExportBuckets(context.Background(), *currentModel.ProjectId).Execute() if err != nil { return progressevent.GetFailedEventByResponse(err.Error(), resp), nil } - resultList := make([]interface{}, 0) + resultList := make([]any, 0) - for i := range output.Results { - model := Model{ - ProjectId: currentModel.ProjectId, - Profile: currentModel.Profile, + if output.Results != nil { + for i := range *output.Results { + model := Model{ + ProjectId: currentModel.ProjectId, + Profile: currentModel.Profile, + } + model.updateModel(&(*output.Results)[i]) + resultList = append(resultList, model) } - model.updateModel(&output.Results[i]) - resultList = append(resultList, model) } return handler.ProgressEvent{ @@ -160,8 +162,9 @@ func List(req handler.Request, prevModel *Model, currentModel *Model) (handler.P }, nil } -func (m *Model) updateModel(bucket *admin20231115002.DiskBackupSnapshotAWSExportBucket) { - m.BucketName = bucket.BucketName +func (m *Model) updateModel(bucket *admin.DiskBackupSnapshotExportBucketResponse) { + m.Id = &bucket.Id + m.BucketName = &bucket.BucketName m.IamRoleID = bucket.IamRoleId - m.Id = bucket.Id + m.RequirePrivateNetworking = bucket.RequirePrivateNetworking } diff --git a/cfn-resources/cloud-backup-snapshot-export-bucket/docs/README.md b/cfn-resources/cloud-backup-snapshot-export-bucket/docs/README.md index ee22d7b3e..afc2e6508 100644 --- a/cfn-resources/cloud-backup-snapshot-export-bucket/docs/README.md +++ b/cfn-resources/cloud-backup-snapshot-export-bucket/docs/README.md @@ -16,6 +16,7 @@ To declare this entity in your AWS CloudFormation template, use the following sy "BucketName" : String, "ProjectId" : String, "IamRoleID" : String, + "RequirePrivateNetworking" : Boolean, } } @@ -29,6 +30,7 @@ Properties: BucketName: String ProjectId: String IamRoleID: String + RequirePrivateNetworking: Boolean ## Properties @@ -61,9 +63,9 @@ _Required_: Yes _Type_: String -_Minimum_: 24 +_Minimum Length_: 24 -_Maximum_: 24 +_Maximum Length_: 24 _Pattern_: ^([a-f0-9]{24})$ @@ -77,14 +79,24 @@ _Required_: Yes _Type_: String -_Minimum_: 24 +_Minimum Length_: 24 -_Maximum_: 24 +_Maximum Length_: 24 _Pattern_: ^([a-f0-9]{24})$ _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) +#### RequirePrivateNetworking + +Indicates whether to do exports over PrivateLink as opposed to public IPs. Defaults to false. + +_Required_: No + +_Type_: Boolean + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + ## Return Values ### Fn::GetAtt diff --git a/cfn-resources/cloud-backup-snapshot-export-bucket/mongodb-atlas-cloudbackupsnapshotexportbucket.json b/cfn-resources/cloud-backup-snapshot-export-bucket/mongodb-atlas-cloudbackupsnapshotexportbucket.json index 510b4042b..24bcef53d 100644 --- a/cfn-resources/cloud-backup-snapshot-export-bucket/mongodb-atlas-cloudbackupsnapshotexportbucket.json +++ b/cfn-resources/cloud-backup-snapshot-export-bucket/mongodb-atlas-cloudbackupsnapshotexportbucket.json @@ -26,6 +26,10 @@ "minLength": 24, "pattern": "^([a-f0-9]{24})$" }, + "RequirePrivateNetworking": { + "type": "boolean", + "description": "Indicates whether to do exports over PrivateLink as opposed to public IPs. Defaults to false." + }, "Id": { "type": "string", "description": "Unique 24-hexadecimal character string that identifies the Amazon Web Services (AWS) Simple Storage Service (S3) export bucket.", diff --git a/cfn-resources/cloud-backup-snapshot-export-bucket/resource-role.yaml b/cfn-resources/cloud-backup-snapshot-export-bucket/resource-role.yaml index 206e0482b..8315e6ac0 100644 --- a/cfn-resources/cloud-backup-snapshot-export-bucket/resource-role.yaml +++ b/cfn-resources/cloud-backup-snapshot-export-bucket/resource-role.yaml @@ -9,41 +9,28 @@ Resources: Properties: MaxSessionDuration: 8400 AssumeRolePolicyDocument: - Version: '2012-10-17' + Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: resources.cloudformation.amazonaws.com Action: sts:AssumeRole + Condition: + StringEquals: + aws:SourceAccount: + Ref: AWS::AccountId + StringLike: + aws:SourceArn: + Fn::Sub: arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:type/resource/MongoDB-Atlas-CloudBackupSnapshotExportBucket/* Path: "/" Policies: - PolicyName: ResourceTypePolicy PolicyDocument: - Version: '2012-10-17' + Version: "2012-10-17" Statement: - Effect: Allow Action: - - "secretsmanager:CreateSecret" - - "secretsmanager:DescribeSecret" - - "secretsmanager:GetSecretValue" - - "secretsmanager:PutSecretValue" - - "secretsmanager:UpdateSecretVersionStage" - - "ec2:CreateVpcEndpoint" - - "ec2:DeleteVpcEndpoints" - - "cloudformation:CreateResource" - - "cloudformation:DeleteResource" - - "cloudformation:GetResource" - - "cloudformation:GetResourceRequestStatus" - - "cloudformation:ListResources" - - "cloudformation:UpdateResource" - - "iam:AttachRolePolicy" - - "iam:CreateRole" - - "iam:DeleteRole" - - "iam:GetRole" - - "iam:GetRolePolicy" - - "iam:ListAttachedRolePolicies" - - "iam:ListRolePolicies" - - "iam:PutRolePolicy" + - "secretsmanager:GetSecretValue" Resource: "*" Outputs: ExecutionRoleArn: diff --git a/cfn-resources/cloud-backup-snapshot-export-bucket/test/cfn-test-create-inputs.sh b/cfn-resources/cloud-backup-snapshot-export-bucket/test/cfn-test-create-inputs.sh index 8c47fba7e..228fd19d8 100755 --- a/cfn-resources/cloud-backup-snapshot-export-bucket/test/cfn-test-create-inputs.sh +++ b/cfn-resources/cloud-backup-snapshot-export-bucket/test/cfn-test-create-inputs.sh @@ -2,93 +2,121 @@ # cfn-test-create-inputs.sh # # This tool generates json files in the inputs/ for `cfn test`. +# It creates all required AWS resources (S3 bucket, IAM role, Cloud Provider Access role) # -#set -o errexit -#set -o nounset -#set -o pipefail -function usage { - echo "Creates a new cloud backup export bucket role for the test" -} +set -euo pipefail + +rm -rf inputs +mkdir inputs + +profile="default" +if [ ${MONGODB_ATLAS_PROFILE+x} ]; then + echo "profile set to ${MONGODB_ATLAS_PROFILE}" + profile=${MONGODB_ATLAS_PROFILE} +fi + +projectName="${1:-$PROJECT_NAME}" +echo "$projectName" + +# Use existing project ID if set, otherwise try to find or create project +if [ -n "${MONGODB_ATLAS_PROJECT_ID:-}" ]; then + projectId="${MONGODB_ATLAS_PROJECT_ID}" + echo -e "Using existing project ID from MONGODB_ATLAS_PROJECT_ID: ${projectId}\n" +else + projectId=$(atlas projects list --output json | jq --arg NAME "${projectName}" -r '.results[] | select(.name==$NAME) | .id') + if [ -z "$projectId" ]; then + projectId=$(atlas projects create "${projectName}" --output=json | jq -r '.id') + echo -e "Created project \"${projectName}\" with id: ${projectId}\n" + else + echo -e "FOUND project \"${projectName}\" with id: ${projectId}\n" + fi +fi +echo -e "=====\nrun this command to clean up\n=====\natlas projects delete ${projectId} --force\n=====" region=$AWS_DEFAULT_REGION awsRegion=$AWS_DEFAULT_REGION if [ -z "$region" ]; then region=$(aws configure get region) + awsRegion=$region fi +regionFormatted=$(echo "$region" | sed -e "s/-/_/g" | tr '[:lower:]' '[:upper:]') +echo "Using region: $region (formatted: $regionFormatted)" -# shellcheck disable=SC2001 -region=$(echo "$region" | sed -e "s/-/_/g") -region=$(echo "$region" | tr '[:lower:]' '[:upper:]') -echo "$region" +# Use mongodb-atlas-cfn-test-* naming convention (allowed by AWS policy, same as log integration) +bucketTag="${CFN_TEST_TAG:-$(date +%Y%m%d%H%M%S)}" +bucketName="mongodb-atlas-cfn-test-export-bucket-${bucketTag}" +roleName="mongodb-atlas-cloud-backup-export-bucket-$(date +%s)-${RANDOM}" +policyName="atlas-cloud-backup-export-bucket-S3-role-policy-${regionFormatted}" -roleName="mongodb-test-cloud-backup-export-bucket-role-${region}" -policyName="atlas-cloud-backup-export-bucket-S3-role-policy-${region}" +echo "Bucket name: ${bucketName}" +echo "Creating IAM role: ${roleName}" -echo "roleName: ${roleName} , policyName: ${policyName}" - -projectName="${1}" -projectId=$(atlas projects list --output json | jq --arg NAME "${projectName}" -r '.results[] | select(.name==$NAME) | .id') -if [ -z "$projectId" ]; then - projectId=$(atlas projects create "${projectName}" --output=json | jq -r '.id') - - echo -e "Created project \"${projectName}\" with id: ${projectId}\n" -else - echo -e "FOUND project \"${projectName}\" with id: ${projectId}\n" -fi - -#------------ CREATING AtlAS ROLE ------------------- +# Create cloud provider access entry roleID=$(atlas cloudProviders accessRoles aws create --projectId "${projectId}" --output json | jq -r '.roleId') -echo -e "--------------------------------Mongo CLI Role creation ends ----------------------------\n" +echo "Created Atlas cloud provider access entry: ${roleID}" + +# Get Atlas AWS Account ARN and External ID +atlasAWSAccountArn=$(atlas cloudProviders accessRoles list --projectId "${projectId}" --output json | jq --arg roleID "${roleID}" -r '.awsIamRoles[] | select(.roleId | test($roleID)) | .atlasAWSAccountArn') +atlasAssumedRoleExternalId=$(atlas cloudProviders accessRoles list --projectId "${projectId}" --output json | jq --arg roleID "${roleID}" -r '.awsIamRoles[] | select(.roleId | test($roleID)) | .atlasAssumedRoleExternalId') -#------------ Get role information------------------- -atlasAWSAccountArn=$(atlas cloudProviders accessRoles list --projectId "${projectId}" --output json | jq --arg roleID "${roleID}" -r '.awsIamRoles[] |select(.roleId |test( $roleID)) |.atlasAWSAccountArn') -atlasAssumedRoleExternalId=$(atlas cloudProviders accessRoles --projectId "${projectId}" list --output json | jq --arg roleID "${roleID}" -r '.awsIamRoles[] |select(.roleId |test( $roleID)) |.atlasAssumedRoleExternalId') +# Create trust policy jq --arg atlasAssumedRoleExternalId "$atlasAssumedRoleExternalId" \ --arg atlasAWSAccountArn "$atlasAWSAccountArn" \ - '.Statement[0].Principal.AWS?|=$atlasAWSAccountArn | .Statement[0].Condition.StringEquals["sts:ExternalId"]?|=$atlasAssumedRoleExternalId' "$(dirname "$0")/role-policy-template.json" >"$(dirname "$0")/add-policy.json" + '.Statement[0].Principal.AWS?|=$atlasAWSAccountArn | .Statement[0].Condition.StringEquals["sts:ExternalId"]?|=$atlasAssumedRoleExternalId' \ + "$(dirname "$0")/role-policy-template.json" >"$(dirname "$0")/add-policy.json" -#------------ Create aws Iam role------------------- +echo "--------------------------------AWS Role creation starts----------------------------" -awsRoleID=$(aws iam get-role --role-name "${roleName}" | jq --arg roleName "${roleName}" -r '.Role | select(.RoleName==$roleName) |.RoleId') -if [ -z "$awsRoleID" ]; then - awsRoleID=$(aws iam create-role --role-name "${roleName}" --assume-role-policy-document "file://$(dirname "$0")/add-policy.json" | jq --arg roleName "${roleName}" -r '.Role | select(.RoleName==$roleName) |.RoleId') - aws iam put-role-policy --role-name "${roleName}" --policy-name "${policyName}" --policy-document "file://$(dirname "$0")/policy.json" - echo -e "No role found, hence creating the role: ${awsRoleID}\n" - - sleep 30 # Role Arn not returning immediately +# Check if role exists, delete if found (must remove inline policy first) +awsRoleId=$(aws iam get-role --role-name "${roleName}" 2>/dev/null | jq -r '.Role.RoleId' || echo "") +if [ -n "$awsRoleId" ]; then + aws iam delete-role-policy --role-name "${roleName}" --policy-name "${policyName}" 2>/dev/null || true + aws iam delete-role --role-name "${roleName}" + echo "Deleted existing role" fi -echo -e "--------------------------------AWS Role creation ends ----------------------------\n" -#------------ get Role arn------------------- -awsArn=$(aws iam get-role --role-name "${roleName}" | jq --arg roleName "${roleName}" -r '.Role | select(.RoleName==$roleName) |.Arn') +# Create IAM role +awsRoleId=$(aws iam create-role --role-name "${roleName}" --assume-role-policy-document file://"$(dirname "$0")/add-policy.json" | jq -r '.Role.RoleId') +echo "Created AWS IAM role: ${awsRoleId}" -echo -e "--------------------------------attach mongodb Role to AWS Role ends ----------------------------\n" +# Get role ARN +awsRoleArn=$(aws iam get-role --role-name "${roleName}" | jq -r '.Role.Arn') -# shellcheck disable=SC2001 -awsArne=$(echo "${awsArn}" | sed 's/"//g') -# shellcheck disable=SC2086 -# TODO Needs change to while loop using get operation -sleep 30 +echo "--------------------------------AWS Role creation ends----------------------------" -atlas cloudProviders accessRoles aws authorize "${roleID}" --projectId "${projectId}" --iamAssumedRoleArn "${awsArne}" -echo -e "--------------------------------authorize mongodb Role ends ----------------------------\n" +# Wait for AWS IAM role to propagate (similar to encryption-at-rest / stream-connection pattern) +echo "Waiting for IAM role to propagate..." +sleep 65 -#create the s3 bucket +# Authorize the role in Atlas +echo "--------------------------------Authorize MongoDB Atlas Role starts----------------------------" +atlas cloudProviders accessRoles aws authorize "${roleID}" --projectId "${projectId}" --iamAssumedRoleArn "${awsRoleArn}" +echo "Authorized role: ${roleName}" +echo "--------------------------------Authorize MongoDB Atlas Role ends----------------------------" -bucketName="cloud-backup-snapshot-${CFN_TEST_TAG}-${awsRegion}" +echo "--------------------------------Creating S3 Bucket----------------------------" +if aws s3 ls "s3://${bucketName}" 2>/dev/null; then + aws s3 rb "s3://${bucketName}" --force +fi +aws s3 mb "s3://${bucketName}" --region "${awsRegion}" +echo "Created S3 bucket: ${bucketName}" -aws s3 rb "s3://${bucketName}" --force -aws s3 mb "s3://${bucketName}" --output json +echo "--------------------------------Attaching S3 policy to IAM role----------------------------" +aws iam put-role-policy \ + --role-name "${roleName}" \ + --policy-name "${policyName}" \ + --policy-document "file://$(dirname "$0")/policy.json" +echo "--------------------------------attach mongodb Role to AWS Role ends----------------------------" -if [ "$#" -ne 2 ]; then usage; fi -if [[ "$*" == help ]]; then usage; fi - -rm -rf inputs -mkdir inputs +# Save role name for cleanup (delete script reads this when run from resource root) +echo "${roleName}" > "$(dirname "$0")/role-name.txt" jq --arg projectId "$projectId" \ - --arg iamRoleID "$roleID" \ --arg bucketName "$bucketName" \ - '.ProjectId?|=$projectId | .IamRoleID?|=$iamRoleID | .BucketName?|=$bucketName ' \ + --arg iamRoleID "$roleID" \ + --arg profile "$profile" \ + '.Profile?|=$profile | .ProjectId?|=$projectId | .BucketName?|=$bucketName | .IamRoleID?|=$iamRoleID' \ "$(dirname "$0")/inputs_1_create.template.json" >"inputs/inputs_1_create.json" + +ls -l inputs diff --git a/cfn-resources/cloud-backup-snapshot-export-bucket/test/cfn-test-delete-inputs.sh b/cfn-resources/cloud-backup-snapshot-export-bucket/test/cfn-test-delete-inputs.sh index c082f894e..b96fe465c 100755 --- a/cfn-resources/cloud-backup-snapshot-export-bucket/test/cfn-test-delete-inputs.sh +++ b/cfn-resources/cloud-backup-snapshot-export-bucket/test/cfn-test-delete-inputs.sh @@ -1,48 +1,60 @@ #!/usr/bin/env bash -# cfn-test-create-inputs.sh +# cfn-test-delete-inputs.sh # -# This tool generates json files in the inputs/ for `cfn test`. +# This tool deletes the mongodb and AWS resources used for `cfn test` as inputs. +# Run from resource root (e.g. make delete-test-resources). # -echo "--------------------------------delete key and key policy document policy document starts ----------------------------" -projectName="${1}" -projectId=$(atlas projects list --output json | jq --arg NAME "${projectName}" -r '.results[] | select(.name==$NAME) | .id') -echo "Check if a project is created $projectId" -export MCLI_PROJECT_ID=$projectId +set -euo pipefail + +projectId=$(jq -r '.ProjectId' ./inputs/inputs_1_create.json) +bucketName=$(jq -r '.BucketName' ./inputs/inputs_1_create.json) +roleId=$(jq -r '.IamRoleID' ./inputs/inputs_1_create.json) + +echo "Deleting resources for projectId: ${projectId}" + +# Get IAM role name from file saved by create script +scriptDir="$(dirname "$0")" +if [ -f "${scriptDir}/role-name.txt" ]; then + roleName=$(cat "${scriptDir}/role-name.txt") + echo "Using role name from role-name.txt: ${roleName}" +else + echo "role-name.txt not found, skipping IAM role cleanup" + roleName="" +fi keyRegion=$AWS_DEFAULT_REGION -awsRegion=$AWS_DEFAULT_REGION if [ -z "$keyRegion" ]; then keyRegion=$(aws configure get region) fi # shellcheck disable=SC2001 -keyRegion=$(echo "$keyRegion" | sed -e "s/-/_/g") -keyRegion=$(echo "$keyRegion" | tr '[:lower:]' '[:upper:]') -echo "$keyRegion" - -roleName="mongodb-test-cloud-backup-export-bucket-role-${keyRegion}" +keyRegion=$(echo "$keyRegion" | sed -e "s/-/_/g" | tr '[:lower:]' '[:upper:]') policyName="atlas-cloud-backup-export-bucket-S3-role-policy-${keyRegion}" -pwd -trustPolicy=$(jq '.Statement[0].Condition.StringEquals["sts:ExternalId"]' "add-policy.json") -echo "$trustPolicy" -roleExternalID=$(${trustPolicy##*/}) -# shellcheck disable=SC2001 -atlasAssumedRoleExternalID=$(echo "${roleExternalID}" | sed 's/"//g') -echo "$atlasAssumedRoleExternalID" - -roleId=$(atlas cloudProviders accessRoles list --output json --projectId "${projectId}" | jq --arg roleID "${atlasAssumedRoleExternalID}" -r '.awsIamRoles[] |select(.atlasAssumedRoleExternalId |test( $roleID)) |.roleId') -echo "$roleId" - -atlas cloudProviders accessRoles aws deauthorize "${roleId}" --projectId "${projectId}" --force -echo "--------------------------------delete role starts ----------------------------" +# Deauthorize Atlas role +if [ -n "$roleId" ] && [ "$roleId" != "null" ]; then + echo "Deauthorizing Atlas role: ${roleId}" + atlas cloudProviders accessRoles aws deauthorize "${roleId}" --projectId "${projectId}" --force || echo "Failed to deauthorize role" +fi -aws iam delete-role-policy --role-name "$roleName" --policy-name "$policyName" -aws iam delete-role --role-name "$roleName" -echo "--------------------------------delete role ends ----------------------------" +# Delete AWS IAM role +if [ -n "$roleName" ] && [ "$roleName" != "" ]; then + echo "--------------------------------delete AWS IAM role starts----------------------------" + aws iam delete-role-policy --role-name "$roleName" --policy-name "$policyName" 2>/dev/null || true + aws iam delete-role --role-name "$roleName" 2>/dev/null || true + echo "--------------------------------delete AWS IAM role ends----------------------------" +fi -bucketName="cloud-backup-snapshot-${CFN_TEST_TAG}-${awsRegion}" +# Delete S3 bucket +if [ -n "$bucketName" ] && [ "$bucketName" != "null" ]; then + echo "Deleting S3 bucket: ${bucketName}" + aws s3 rb "s3://${bucketName}" --force 2>/dev/null || true +fi -aws s3 rb s3://"${bucketName}" --force -echo "--------------------------------delete bucket ends ----------------------------" -#mongocli iam projects delete "${projectId}" --force +# Delete project +if atlas projects delete "$projectId" --force; then + echo "${projectId} project deletion OK" +else + echo "Failed cleaning project: ${projectId}" + exit 1 +fi diff --git a/cfn-resources/cloud-backup-snapshot-export-bucket/test/contract-testing/cfn-test-create.sh b/cfn-resources/cloud-backup-snapshot-export-bucket/test/contract-testing/cfn-test-create.sh new file mode 100755 index 000000000..5161b2ed7 --- /dev/null +++ b/cfn-resources/cloud-backup-snapshot-export-bucket/test/contract-testing/cfn-test-create.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# This tool generates the resources and json files in the inputs/ for `cfn test`. +set -o errexit +set -o nounset +set -o pipefail + +if [ -z "${AWS_DEFAULT_REGION+x}" ]; then + echo "AWS_DEFAULT_REGION must be set" + exit 1 +fi + +projectName="cfn-export-bucket-$(date +%s)-$RANDOM" + +echo "projectName: $projectName" + +./test/cfn-test-create-inputs.sh "$projectName" diff --git a/cfn-resources/cloud-backup-snapshot-export-bucket/test/contract-testing/cfn-test-delete.sh b/cfn-resources/cloud-backup-snapshot-export-bucket/test/contract-testing/cfn-test-delete.sh new file mode 100755 index 000000000..071210dd9 --- /dev/null +++ b/cfn-resources/cloud-backup-snapshot-export-bucket/test/contract-testing/cfn-test-delete.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# This tool deletes the mongodb and AWS resources used for `cfn test` as inputs. +set -o errexit +set -o nounset +set -o pipefail + +# Run from resource root so ./inputs/ and ./test/ exist +scriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +resourceRoot="$(dirname "$(dirname "$scriptDir")")" +cd "$resourceRoot" + +./test/cfn-test-delete-inputs.sh diff --git a/cfn-resources/cloud-backup-snapshot-export-bucket/test/inputs_1_create.template.json b/cfn-resources/cloud-backup-snapshot-export-bucket/test/inputs_1_create.template.json index 1240fe76a..50a6a9c23 100644 --- a/cfn-resources/cloud-backup-snapshot-export-bucket/test/inputs_1_create.template.json +++ b/cfn-resources/cloud-backup-snapshot-export-bucket/test/inputs_1_create.template.json @@ -2,5 +2,6 @@ "ProjectId": "", "IamRoleID": "", "BucketName": "", - "Profile": "default" + "Profile": "default", + "RequirePrivateNetworking": "false" } diff --git a/examples/cloud-backup-snapshot-export-bucket/CloudBackupSnapshotExportBucket.json b/examples/cloud-backup-snapshot-export-bucket/CloudBackupSnapshotExportBucket.json index 5bd212ff4..266301b9f 100644 --- a/examples/cloud-backup-snapshot-export-bucket/CloudBackupSnapshotExportBucket.json +++ b/examples/cloud-backup-snapshot-export-bucket/CloudBackupSnapshotExportBucket.json @@ -36,7 +36,8 @@ }, "Profile": { "Ref": "Profile" - } + }, + "RequirePrivateNetworking": false } } },