From 44499b75f61546a33baddd6c83d3d385d0fd4d60 Mon Sep 17 00:00:00 2001 From: sivaram-mongodb Date: Fri, 16 Jan 2026 15:49:18 +0530 Subject: [PATCH 1/5] feat: Backup Compliance Policy CloudFormation Resource --- .../backup-compliance-policy/.rpdk-config | 12 + .../backup-compliance-policy/Makefile | 29 ++ .../backup-compliance-policy/README.md | 18 + .../backup-compliance-policy/cmd/main.go | 85 +++++ .../cmd/resource/config.go | 19 + .../cmd/resource/mappings.go | 254 ++++++++++++++ .../cmd/resource/mappings_test.go | 261 ++++++++++++++ .../cmd/resource/model.go | 43 +++ .../cmd/resource/resource.go | 84 +++++ .../cmd/resource/share.go | 325 ++++++++++++++++++ .../backup-compliance-policy/docs/README.md | 280 +++++++++++++++ .../docs/ondemandpolicyitem.md | 60 ++++ .../docs/scheduledpolicyitem.md | 86 +++++ .../mongodb-atlas-backupcompliancepolicy.json | 203 +++++++++++ .../resource-role.yaml | 38 ++ .../backup-compliance-policy/template.yml | 27 ++ ...-compliance-policy.sample-cfn-request.json | 13 + .../test/cfn-test-create-inputs.sh | 56 +++ .../test/cfn-test-delete-inputs.sh | 4 + .../test/contract-testing/cfn-test-create.sh | 24 ++ .../test/contract-testing/cfn-test-delete.sh | 15 + .../test/inputs_1_create.template.json | 7 + .../test/inputs_1_update.template.json | 8 + cfn-resources/util/constants/constants.go | 4 + examples/backup-compliance-policy/README.md | 25 ++ .../backup-compliance-policy.json | 165 +++++++++ 26 files changed, 2145 insertions(+) create mode 100644 cfn-resources/backup-compliance-policy/.rpdk-config create mode 100644 cfn-resources/backup-compliance-policy/Makefile create mode 100644 cfn-resources/backup-compliance-policy/README.md create mode 100644 cfn-resources/backup-compliance-policy/cmd/main.go create mode 100644 cfn-resources/backup-compliance-policy/cmd/resource/config.go create mode 100644 cfn-resources/backup-compliance-policy/cmd/resource/mappings.go create mode 100644 cfn-resources/backup-compliance-policy/cmd/resource/mappings_test.go create mode 100644 cfn-resources/backup-compliance-policy/cmd/resource/model.go create mode 100644 cfn-resources/backup-compliance-policy/cmd/resource/resource.go create mode 100644 cfn-resources/backup-compliance-policy/cmd/resource/share.go create mode 100644 cfn-resources/backup-compliance-policy/docs/README.md create mode 100644 cfn-resources/backup-compliance-policy/docs/ondemandpolicyitem.md create mode 100644 cfn-resources/backup-compliance-policy/docs/scheduledpolicyitem.md create mode 100644 cfn-resources/backup-compliance-policy/mongodb-atlas-backupcompliancepolicy.json create mode 100644 cfn-resources/backup-compliance-policy/resource-role.yaml create mode 100644 cfn-resources/backup-compliance-policy/template.yml create mode 100644 cfn-resources/backup-compliance-policy/test/backup-compliance-policy.sample-cfn-request.json create mode 100755 cfn-resources/backup-compliance-policy/test/cfn-test-create-inputs.sh create mode 100755 cfn-resources/backup-compliance-policy/test/cfn-test-delete-inputs.sh create mode 100755 cfn-resources/backup-compliance-policy/test/contract-testing/cfn-test-create.sh create mode 100755 cfn-resources/backup-compliance-policy/test/contract-testing/cfn-test-delete.sh create mode 100644 cfn-resources/backup-compliance-policy/test/inputs_1_create.template.json create mode 100644 cfn-resources/backup-compliance-policy/test/inputs_1_update.template.json create mode 100644 examples/backup-compliance-policy/README.md create mode 100644 examples/backup-compliance-policy/backup-compliance-policy.json diff --git a/cfn-resources/backup-compliance-policy/.rpdk-config b/cfn-resources/backup-compliance-policy/.rpdk-config new file mode 100644 index 000000000..9b9328eff --- /dev/null +++ b/cfn-resources/backup-compliance-policy/.rpdk-config @@ -0,0 +1,12 @@ +{ + "typeName": "MongoDB::Atlas::BackupCompliancePolicy", + "language": "go", + "runtime": "provided.al2", + "entrypoint": "bootstrap", + "testEntrypoint": "bootstrap", + "settings": { + "import_path": "github.com/mongodb/mongodbatlas-cloudformation-resources/backup-compliance-policy", + "protocolVersion": "2.0.0", + "pluginVersion": "2.0.4" + } +} diff --git a/cfn-resources/backup-compliance-policy/Makefile b/cfn-resources/backup-compliance-policy/Makefile new file mode 100644 index 000000000..56b0b3c6f --- /dev/null +++ b/cfn-resources/backup-compliance-policy/Makefile @@ -0,0 +1,29 @@ +.PHONY: build test clean +tags=logging callback metrics scheduler +cgo=0 +goos=linux +goarch=amd64 +CFNREP_GIT_SHA?=$(shell git rev-parse HEAD) +ldXflags=-s -w -X github.com/mongodb/mongodbatlas-cloudformation-resources/util.defaultLogLevel=info -X github.com/mongodb/mongodbatlas-cloudformation-resources/version.Version=${CFNREP_GIT_SHA} +ldXflagsD=-X github.com/mongodb/mongodbatlas-cloudformation-resources/util.defaultLogLevel=debug -X github.com/mongodb/mongodbatlas-cloudformation-resources/version.Version=${CFNREP_GIT_SHA} + +build: + cfn generate + env GOOS=$(goos) CGO_ENABLED=$(cgo) GOARCH=$(goarch) go build -ldflags="$(ldXflags)" -tags="$(tags)" -o bin/bootstrap cmd/main.go + +debug: + cfn generate + env GOOS=$(goos) CGO_ENABLED=$(cgo) GOARCH=$(goarch) go build -ldflags="$(ldXflagsD)" -tags="$(tags)" -o bin/bootstrap cmd/main.go + +clean: + rm -rf bin + +create-test-resources: + @echo "==> Creating test files for contract testing" + ./test/contract-testing/cfn-test-create-inputs.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/backup-compliance-policy/README.md b/cfn-resources/backup-compliance-policy/README.md new file mode 100644 index 000000000..4cafba23d --- /dev/null +++ b/cfn-resources/backup-compliance-policy/README.md @@ -0,0 +1,18 @@ +# MongoDB::Atlas::BackupCompliancePolicy + +## Description + +Resource for managing [Backup Compliance Policy](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/group/endpoint-cloud-backups/operation/updateCompliancePolicy). Backup Compliance Policy prevents any user, regardless of role, from modifying or deleting specific cluster settings, backups, and backup configurations. When enabled, the Backup Compliance Policy will be applied as the minimum policy for all clusters and backups in the project. + +## Requirements + +To securely give CloudFormation access to your Atlas credentials, you must +set up an [AWS Profile](/README.md#mongodb-atlas-api-keys-credential-management). + +## Attributes and Parameters + +See the [resource docs](docs/README.md). + +## Cloudformation Examples + +See the examples [CFN Template](/examples/backup-compliance-policy/README.md) for example resource. diff --git a/cfn-resources/backup-compliance-policy/cmd/main.go b/cfn-resources/backup-compliance-policy/cmd/main.go new file mode 100644 index 000000000..7736f1876 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/cmd/main.go @@ -0,0 +1,85 @@ +// Code generated by 'cfn generate', changes will be undone by the next invocation. DO NOT EDIT. +package main + +import ( + "errors" + "fmt" + "log" + + "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn" + "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" + "github.com/mongodb/mongodbatlas-cloudformation-resources/backup-compliance-policy/cmd/resource" +) + +// Handler is a container for the CRUDL actions exported by resources +type Handler struct{} + +// Create wraps the related Create function exposed by the resource code +func (r *Handler) Create(req handler.Request) handler.ProgressEvent { + return wrap(req, resource.Create) +} + +// Read wraps the related Read function exposed by the resource code +func (r *Handler) Read(req handler.Request) handler.ProgressEvent { + return wrap(req, resource.Read) +} + +// Update wraps the related Update function exposed by the resource code +func (r *Handler) Update(req handler.Request) handler.ProgressEvent { + return wrap(req, resource.Update) +} + +// Delete wraps the related Delete function exposed by the resource code +func (r *Handler) Delete(req handler.Request) handler.ProgressEvent { + return wrap(req, resource.Delete) +} + +// List wraps the related List function exposed by the resource code +func (r *Handler) List(req handler.Request) handler.ProgressEvent { + return wrap(req, resource.List) +} + +// main is the entry point of the application. +func main() { + cfn.Start(&Handler{}) +} + +type handlerFunc func(handler.Request, *resource.Model, *resource.Model) (handler.ProgressEvent, error) + +func wrap(req handler.Request, f handlerFunc) (response handler.ProgressEvent) { + defer func() { + // Catch any panics and return a failed ProgressEvent + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = errors.New(fmt.Sprint(r)) + } + + log.Printf("Trapped error in handler: %v", err) + + response = handler.NewFailedEvent(err) + } + }() + + // Populate the previous model + prevModel := &resource.Model{} + if err := req.UnmarshalPrevious(prevModel); err != nil { + log.Printf("Error unmarshaling prev model: %v", err) + return handler.NewFailedEvent(err) + } + + // Populate the current model + currentModel := &resource.Model{} + if err := req.Unmarshal(currentModel); err != nil { + log.Printf("Error unmarshaling model: %v", err) + return handler.NewFailedEvent(err) + } + + response, err := f(req, prevModel, currentModel) + if err != nil { + log.Printf("Error returned from handler function: %v", err) + return handler.NewFailedEvent(err) + } + + return response +} diff --git a/cfn-resources/backup-compliance-policy/cmd/resource/config.go b/cfn-resources/backup-compliance-policy/cmd/resource/config.go new file mode 100644 index 000000000..4d9eb7831 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/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/backup-compliance-policy/cmd/resource/mappings.go b/cfn-resources/backup-compliance-policy/cmd/resource/mappings.go new file mode 100644 index 000000000..bdd140a65 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/cmd/resource/mappings.go @@ -0,0 +1,254 @@ +// Copyright 2026 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 resource + +import ( + "go.mongodb.org/atlas-sdk/v20250312012/admin" + + "github.com/mongodb/mongodbatlas-cloudformation-resources/util" +) + +const ( + Hourly = "hourly" + Daily = "daily" + Weekly = "weekly" + Monthly = "monthly" + Yearly = "yearly" +) + +func isOnDemandPolicyItemEmpty(item *OnDemandPolicyItem) bool { + return item == nil || (item.FrequencyInterval == nil && item.RetentionUnit == nil && item.RetentionValue == nil) +} + +func isScheduledPolicyItemEmpty(item *ScheduledPolicyItem) bool { + return item == nil || (item.FrequencyInterval == nil && item.RetentionUnit == nil && item.RetentionValue == nil) +} + +func SetBackupCompliancePolicyData(currentModel *Model, policy *admin.DataProtectionSettings20231001) { + if policy == nil { + return + } + + if policy.ProjectId != nil { + currentModel.ProjectId = policy.ProjectId + } + + authorizedEmail := policy.GetAuthorizedEmail() + currentModel.AuthorizedEmail = &authorizedEmail + authorizedUserFirstName := policy.GetAuthorizedUserFirstName() + currentModel.AuthorizedUserFirstName = &authorizedUserFirstName + authorizedUserLastName := policy.GetAuthorizedUserLastName() + currentModel.AuthorizedUserLastName = &authorizedUserLastName + + currentModel.CopyProtectionEnabled = policy.CopyProtectionEnabled + currentModel.EncryptionAtRestEnabled = policy.EncryptionAtRestEnabled + currentModel.PitEnabled = policy.PitEnabled + currentModel.RestoreWindowDays = policy.RestoreWindowDays + + if policy.OnDemandPolicyItem != nil { + currentModel.OnDemandPolicyItem = GetOnDemandPolicyItem(policy.OnDemandPolicyItem) + } + + if policy.ScheduledPolicyItems != nil { + currentModel.PolicyItemHourly = nil + currentModel.PolicyItemDaily = nil + currentModel.PolicyItemWeekly = []ScheduledPolicyItem{} + currentModel.PolicyItemMonthly = []ScheduledPolicyItem{} + currentModel.PolicyItemYearly = []ScheduledPolicyItem{} + + for _, item := range *policy.ScheduledPolicyItems { + frequencyType := item.GetFrequencyType() + scheduledItem := GetScheduledPolicyItem(&item) + switch frequencyType { + case Hourly: + currentModel.PolicyItemHourly = scheduledItem + case Daily: + currentModel.PolicyItemDaily = scheduledItem + case Weekly: + currentModel.PolicyItemWeekly = append(currentModel.PolicyItemWeekly, *scheduledItem) + case Monthly: + currentModel.PolicyItemMonthly = append(currentModel.PolicyItemMonthly, *scheduledItem) + case Yearly: + currentModel.PolicyItemYearly = append(currentModel.PolicyItemYearly, *scheduledItem) + } + } + } + + state := policy.GetState() + currentModel.State = &state + if policy.UpdatedDate != nil { + updatedDateStr := util.TimeToString(*policy.UpdatedDate) + currentModel.UpdatedDate = &updatedDateStr + } + updatedUser := policy.GetUpdatedUser() + currentModel.UpdatedUser = &updatedUser +} + +func GetOnDemandPolicyItem(item *admin.BackupComplianceOnDemandPolicyItem) *OnDemandPolicyItem { + if item == nil { + return nil + } + frequencyType := item.GetFrequencyType() + retentionUnit := item.GetRetentionUnit() + frequencyInterval := item.GetFrequencyInterval() + retentionValue := item.GetRetentionValue() + return &OnDemandPolicyItem{ + Id: item.Id, + FrequencyInterval: &frequencyInterval, + FrequencyType: &frequencyType, + RetentionUnit: &retentionUnit, + RetentionValue: &retentionValue, + } +} + +func GetScheduledPolicyItem(item *admin.BackupComplianceScheduledPolicyItem) *ScheduledPolicyItem { + if item == nil { + return nil + } + frequencyType := item.GetFrequencyType() + retentionUnit := item.GetRetentionUnit() + frequencyInterval := item.GetFrequencyInterval() + retentionValue := item.GetRetentionValue() + return &ScheduledPolicyItem{ + Id: item.Id, + FrequencyType: &frequencyType, + FrequencyInterval: &frequencyInterval, + RetentionUnit: &retentionUnit, + RetentionValue: &retentionValue, + } +} + +func ExpandDataProtectionSettings(model *Model, projectID string) *admin.DataProtectionSettings20231001 { + authorizedEmail := "" + if model.AuthorizedEmail != nil { + authorizedEmail = *model.AuthorizedEmail + } + authorizedUserFirstName := "" + if model.AuthorizedUserFirstName != nil { + authorizedUserFirstName = *model.AuthorizedUserFirstName + } + authorizedUserLastName := "" + if model.AuthorizedUserLastName != nil { + authorizedUserLastName = *model.AuthorizedUserLastName + } + + copyProtectionEnabled := false + if model.CopyProtectionEnabled != nil { + copyProtectionEnabled = *model.CopyProtectionEnabled + } + encryptionAtRestEnabled := false + if model.EncryptionAtRestEnabled != nil { + encryptionAtRestEnabled = *model.EncryptionAtRestEnabled + } + pitEnabled := false + if model.PitEnabled != nil { + pitEnabled = *model.PitEnabled + } + restoreWindowDays := 0 + if model.RestoreWindowDays != nil { + restoreWindowDays = *model.RestoreWindowDays + } + + settings := &admin.DataProtectionSettings20231001{ + ProjectId: &projectID, + AuthorizedEmail: authorizedEmail, + AuthorizedUserFirstName: authorizedUserFirstName, + AuthorizedUserLastName: authorizedUserLastName, + CopyProtectionEnabled: ©ProtectionEnabled, + EncryptionAtRestEnabled: &encryptionAtRestEnabled, + PitEnabled: &pitEnabled, + RestoreWindowDays: &restoreWindowDays, + } + + if !isOnDemandPolicyItemEmpty(model.OnDemandPolicyItem) { + settings.OnDemandPolicyItem = ExpandOnDemandPolicyItem(model.OnDemandPolicyItem) + } + + var scheduledItems []admin.BackupComplianceScheduledPolicyItem + + if !isScheduledPolicyItemEmpty(model.PolicyItemHourly) { + scheduledItems = append(scheduledItems, ExpandScheduledPolicyItem(model.PolicyItemHourly, Hourly)) + } + if !isScheduledPolicyItemEmpty(model.PolicyItemDaily) { + scheduledItems = append(scheduledItems, ExpandScheduledPolicyItem(model.PolicyItemDaily, Daily)) + } + if len(model.PolicyItemWeekly) > 0 { + for _, item := range model.PolicyItemWeekly { + scheduledItems = append(scheduledItems, ExpandScheduledPolicyItem(&item, Weekly)) + } + } + if len(model.PolicyItemMonthly) > 0 { + for _, item := range model.PolicyItemMonthly { + scheduledItems = append(scheduledItems, ExpandScheduledPolicyItem(&item, Monthly)) + } + } + if len(model.PolicyItemYearly) > 0 { + for _, item := range model.PolicyItemYearly { + scheduledItems = append(scheduledItems, ExpandScheduledPolicyItem(&item, Yearly)) + } + } + + if len(scheduledItems) > 0 { + settings.ScheduledPolicyItems = &scheduledItems + } + + return settings +} + +func ExpandOnDemandPolicyItem(item *OnDemandPolicyItem) *admin.BackupComplianceOnDemandPolicyItem { + if item == nil { + return nil + } + frequencyInterval := 0 + if item.FrequencyInterval != nil { + frequencyInterval = *item.FrequencyInterval + } + retentionValue := 0 + if item.RetentionValue != nil { + retentionValue = *item.RetentionValue + } + retentionUnit := "" + if item.RetentionUnit != nil { + retentionUnit = *item.RetentionUnit + } + return &admin.BackupComplianceOnDemandPolicyItem{ + Id: item.Id, + FrequencyInterval: frequencyInterval, + FrequencyType: "ondemand", + RetentionUnit: retentionUnit, + RetentionValue: retentionValue, + } +} + +func ExpandScheduledPolicyItem(item *ScheduledPolicyItem, frequencyType string) admin.BackupComplianceScheduledPolicyItem { + frequencyInterval := 0 + if item.FrequencyInterval != nil { + frequencyInterval = *item.FrequencyInterval + } + retentionValue := 0 + if item.RetentionValue != nil { + retentionValue = *item.RetentionValue + } + retentionUnit := "" + if item.RetentionUnit != nil { + retentionUnit = *item.RetentionUnit + } + return admin.BackupComplianceScheduledPolicyItem{ + FrequencyType: frequencyType, + FrequencyInterval: frequencyInterval, + RetentionUnit: retentionUnit, + RetentionValue: retentionValue, + } +} diff --git a/cfn-resources/backup-compliance-policy/cmd/resource/mappings_test.go b/cfn-resources/backup-compliance-policy/cmd/resource/mappings_test.go new file mode 100644 index 000000000..f83a54a6b --- /dev/null +++ b/cfn-resources/backup-compliance-policy/cmd/resource/mappings_test.go @@ -0,0 +1,261 @@ +// Copyright 2026 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 resource_test + +import ( + "testing" + "time" + + "github.com/mongodb/mongodbatlas-cloudformation-resources/backup-compliance-policy/cmd/resource" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util" + "github.com/stretchr/testify/assert" + "go.mongodb.org/atlas-sdk/v20250312012/admin" +) + +func TestPolicyItemConversions(t *testing.T) { + t.Run("getOnDemandPolicyItem", func(t *testing.T) { + assert.Nil(t, resource.GetOnDemandPolicyItem(nil)) + + item := &admin.BackupComplianceOnDemandPolicyItem{ + Id: util.StringPtr("id-1"), + FrequencyType: "ondemand", + FrequencyInterval: 1, + RetentionUnit: "days", + RetentionValue: 7, + } + result := resource.GetOnDemandPolicyItem(item) + assert.Equal(t, "id-1", *result.Id) + assert.Equal(t, 1, *result.FrequencyInterval) + assert.Equal(t, 7, *result.RetentionValue) + }) + + t.Run("expandOnDemandPolicyItem", func(t *testing.T) { + assert.Nil(t, resource.ExpandOnDemandPolicyItem(nil)) + + freqInterval := 2 + retentionValue := 4 + model := &resource.OnDemandPolicyItem{ + FrequencyInterval: &freqInterval, + RetentionUnit: util.StringPtr("weeks"), + RetentionValue: &retentionValue, + } + result := resource.ExpandOnDemandPolicyItem(model) + assert.Equal(t, "ondemand", result.FrequencyType) + assert.Equal(t, 2, result.FrequencyInterval) + assert.Equal(t, 4, result.RetentionValue) + }) + + t.Run("getScheduledPolicyItem", func(t *testing.T) { + assert.Nil(t, resource.GetScheduledPolicyItem(nil)) + + item := &admin.BackupComplianceScheduledPolicyItem{ + FrequencyType: "hourly", + FrequencyInterval: 6, + RetentionUnit: "days", + RetentionValue: 3, + } + result := resource.GetScheduledPolicyItem(item) + assert.Equal(t, "hourly", *result.FrequencyType) + assert.Equal(t, 6, *result.FrequencyInterval) + }) + + t.Run("expandScheduledPolicyItem", func(t *testing.T) { + freqInterval := 1 + retentionValue := 4 + model := &resource.ScheduledPolicyItem{ + FrequencyInterval: &freqInterval, + RetentionUnit: util.StringPtr("weeks"), + RetentionValue: &retentionValue, + } + result := resource.ExpandScheduledPolicyItem(model, "daily") + assert.Equal(t, "daily", result.FrequencyType) + assert.Equal(t, 1, result.FrequencyInterval) + }) +} + +func TestSetBackupCompliancePolicyData(t *testing.T) { + tests := map[string]struct { + policy *admin.DataProtectionSettings20231001 + check func(*testing.T, *resource.Model) + }{ + "nil policy": { + policy: nil, + check: func(t *testing.T, m *resource.Model) { + t.Helper() + assert.Nil(t, m.ProjectId) + }, + }, + "basic fields": { + policy: &admin.DataProtectionSettings20231001{ + ProjectId: util.StringPtr("proj-123"), + AuthorizedEmail: "admin@example.com", + CopyProtectionEnabled: util.Pointer(true), + RestoreWindowDays: util.IntPtr(7), + State: util.StringPtr("ACTIVE"), + }, + check: func(t *testing.T, m *resource.Model) { + t.Helper() + assert.Equal(t, "proj-123", *m.ProjectId) + assert.Equal(t, "admin@example.com", *m.AuthorizedEmail) + assert.True(t, *m.CopyProtectionEnabled) + assert.Equal(t, 7, *m.RestoreWindowDays) + assert.Equal(t, "ACTIVE", *m.State) + }, + }, + "with scheduled items": { + policy: &admin.DataProtectionSettings20231001{ + ProjectId: util.StringPtr("proj-456"), + AuthorizedEmail: "test@example.com", + ScheduledPolicyItems: &[]admin.BackupComplianceScheduledPolicyItem{ + {FrequencyType: "hourly", FrequencyInterval: 6, RetentionUnit: "days", RetentionValue: 3}, + {FrequencyType: "daily", FrequencyInterval: 1, RetentionUnit: "weeks", RetentionValue: 1}, + {FrequencyType: "weekly", FrequencyInterval: 1, RetentionUnit: "months", RetentionValue: 1}, + }, + State: util.StringPtr("ACTIVE"), + }, + check: func(t *testing.T, m *resource.Model) { + t.Helper() + assert.NotNil(t, m.PolicyItemHourly) + assert.NotNil(t, m.PolicyItemDaily) + assert.Len(t, m.PolicyItemWeekly, 1) + assert.Equal(t, "hourly", *m.PolicyItemHourly.FrequencyType) + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + model := &resource.Model{} + resource.SetBackupCompliancePolicyData(model, tc.policy) + tc.check(t, model) + }) + } +} + +func TestExpandDataProtectionSettings(t *testing.T) { + tests := map[string]struct { + model *resource.Model + check func(*testing.T, *admin.DataProtectionSettings20231001) + }{ + "with empty policy items": { + model: &resource.Model{ + AuthorizedEmail: util.StringPtr("admin@example.com"), + OnDemandPolicyItem: &resource.OnDemandPolicyItem{}, + PolicyItemHourly: &resource.ScheduledPolicyItem{}, + PolicyItemDaily: &resource.ScheduledPolicyItem{}, + }, + check: func(t *testing.T, s *admin.DataProtectionSettings20231001) { + t.Helper() + assert.Nil(t, s.OnDemandPolicyItem, "empty OnDemandPolicyItem should be omitted") + assert.Nil(t, s.ScheduledPolicyItems, "empty policy items should not create scheduled items") + }, + }, + "minimal model": { + model: &resource.Model{ + AuthorizedEmail: util.StringPtr("admin@example.com"), + AuthorizedUserFirstName: util.StringPtr("Jane"), + AuthorizedUserLastName: util.StringPtr("Smith"), + }, + check: func(t *testing.T, s *admin.DataProtectionSettings20231001) { + t.Helper() + assert.Equal(t, "admin@example.com", s.AuthorizedEmail) + assert.Equal(t, "Jane", s.AuthorizedUserFirstName) + assert.False(t, *s.CopyProtectionEnabled) + }, + }, + "with booleans and integers": { + model: &resource.Model{ + AuthorizedEmail: util.StringPtr("test@example.com"), + CopyProtectionEnabled: util.Pointer(true), + EncryptionAtRestEnabled: util.Pointer(true), + PitEnabled: util.Pointer(false), + RestoreWindowDays: util.IntPtr(14), + }, + check: func(t *testing.T, s *admin.DataProtectionSettings20231001) { + t.Helper() + assert.True(t, *s.CopyProtectionEnabled) + assert.True(t, *s.EncryptionAtRestEnabled) + assert.False(t, *s.PitEnabled) + assert.Equal(t, 14, *s.RestoreWindowDays) + }, + }, + "with all policy items": { + model: &resource.Model{ + AuthorizedEmail: util.StringPtr("admin@example.com"), + PolicyItemHourly: &resource.ScheduledPolicyItem{ + FrequencyInterval: util.IntPtr(6), + RetentionUnit: util.StringPtr("days"), + RetentionValue: util.IntPtr(3), + }, + PolicyItemDaily: &resource.ScheduledPolicyItem{ + FrequencyInterval: util.IntPtr(1), + RetentionUnit: util.StringPtr("weeks"), + RetentionValue: util.IntPtr(1), + }, + PolicyItemWeekly: []resource.ScheduledPolicyItem{ + { + FrequencyInterval: util.IntPtr(1), + RetentionUnit: util.StringPtr("months"), + RetentionValue: util.IntPtr(2), + }, + }, + }, + check: func(t *testing.T, s *admin.DataProtectionSettings20231001) { + t.Helper() + assert.NotNil(t, s.ScheduledPolicyItems) + items := *s.ScheduledPolicyItems + assert.Len(t, items, 3) + assert.Equal(t, "hourly", items[0].FrequencyType) + assert.Equal(t, "daily", items[1].FrequencyType) + assert.Equal(t, "weekly", items[2].FrequencyType) + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := resource.ExpandDataProtectionSettings(tc.model, "test-project") + assert.Equal(t, "test-project", *result.ProjectId) + tc.check(t, result) + }) + } +} + +func TestRoundTripConversion(t *testing.T) { + t.Run("policy survives round trip", func(t *testing.T) { + original := &admin.DataProtectionSettings20231001{ + ProjectId: util.StringPtr("proj-999"), + AuthorizedEmail: "security@example.com", + AuthorizedUserFirstName: "Test", + AuthorizedUserLastName: "User", + CopyProtectionEnabled: util.Pointer(true), + PitEnabled: util.Pointer(false), + RestoreWindowDays: util.IntPtr(7), + State: util.StringPtr("ACTIVE"), + UpdatedDate: &time.Time{}, + UpdatedUser: util.StringPtr("admin"), + } + + model := &resource.Model{} + resource.SetBackupCompliancePolicyData(model, original) + + result := resource.ExpandDataProtectionSettings(model, "proj-999") + + assert.Equal(t, *original.ProjectId, *result.ProjectId) + assert.Equal(t, original.AuthorizedEmail, result.AuthorizedEmail) + assert.Equal(t, *original.CopyProtectionEnabled, *result.CopyProtectionEnabled) + assert.Equal(t, *original.RestoreWindowDays, *result.RestoreWindowDays) + }) +} diff --git a/cfn-resources/backup-compliance-policy/cmd/resource/model.go b/cfn-resources/backup-compliance-policy/cmd/resource/model.go new file mode 100644 index 000000000..dd180c1d9 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/cmd/resource/model.go @@ -0,0 +1,43 @@ +// 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 + +// Model is autogenerated from the json schema +type Model struct { + Profile *string `json:",omitempty"` + ProjectId *string `json:",omitempty"` + AuthorizedEmail *string `json:",omitempty"` + AuthorizedUserFirstName *string `json:",omitempty"` + AuthorizedUserLastName *string `json:",omitempty"` + CopyProtectionEnabled *bool `json:",omitempty"` + EncryptionAtRestEnabled *bool `json:",omitempty"` + RestoreWindowDays *int `json:",omitempty"` + OnDemandPolicyItem *OnDemandPolicyItem `json:",omitempty"` + PitEnabled *bool `json:",omitempty"` + State *string `json:",omitempty"` + UpdatedDate *string `json:",omitempty"` + UpdatedUser *string `json:",omitempty"` + PolicyItemHourly *ScheduledPolicyItem `json:",omitempty"` + PolicyItemDaily *ScheduledPolicyItem `json:",omitempty"` + PolicyItemWeekly []ScheduledPolicyItem `json:",omitempty"` + PolicyItemMonthly []ScheduledPolicyItem `json:",omitempty"` + PolicyItemYearly []ScheduledPolicyItem `json:",omitempty"` +} + +// OnDemandPolicyItem is autogenerated from the json schema +type OnDemandPolicyItem struct { + Id *string `json:",omitempty"` + FrequencyInterval *int `json:",omitempty"` + FrequencyType *string `json:",omitempty"` + RetentionUnit *string `json:",omitempty"` + RetentionValue *int `json:",omitempty"` +} + +// ScheduledPolicyItem is autogenerated from the json schema +type ScheduledPolicyItem struct { + Id *string `json:",omitempty"` + FrequencyType *string `json:",omitempty"` + FrequencyInterval *int `json:",omitempty"` + RetentionUnit *string `json:",omitempty"` + RetentionValue *int `json:",omitempty"` +} diff --git a/cfn-resources/backup-compliance-policy/cmd/resource/resource.go b/cfn-resources/backup-compliance-policy/cmd/resource/resource.go new file mode 100644 index 000000000..77789d6b4 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/cmd/resource/resource.go @@ -0,0 +1,84 @@ +// Copyright 2026 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 resource + +import ( + "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" + + "github.com/mongodb/mongodbatlas-cloudformation-resources/util" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/constants" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/validator" +) + +const callBackSeconds = 10 + +var ( + createRequiredFields = []string{constants.ProjectID, constants.AuthorizedEmail, constants.AuthorizedUserFirstName, constants.AuthorizedUserLastName} + readUpdateDeleteRequiredFields = []string{constants.ProjectID} + listRequiredFields = []string{constants.ProjectID} +) + +func Create(req handler.Request, prevModel *Model, model *Model) (handler.ProgressEvent, error) { + client, setupErr := setupRequest(req, model, createRequiredFields) + if setupErr != nil { + return *setupErr, nil + } + return HandleCreate(&req, client, model), nil +} + +func Read(req handler.Request, prevModel *Model, model *Model) (handler.ProgressEvent, error) { + client, setupErr := setupRequest(req, model, readUpdateDeleteRequiredFields) + if setupErr != nil { + return *setupErr, nil + } + return HandleRead(&req, client, model), nil +} + +func Update(req handler.Request, prevModel *Model, model *Model) (handler.ProgressEvent, error) { + client, setupErr := setupRequest(req, model, readUpdateDeleteRequiredFields) + if setupErr != nil { + return *setupErr, nil + } + return HandleUpdate(&req, client, model), nil +} + +func Delete(req handler.Request, prevModel *Model, model *Model) (handler.ProgressEvent, error) { + client, setupErr := setupRequest(req, model, readUpdateDeleteRequiredFields) + if setupErr != nil { + return *setupErr, nil + } + return HandleDelete(&req, client, model), nil +} + +func List(req handler.Request, prevModel *Model, model *Model) (handler.ProgressEvent, error) { + client, setupErr := setupRequest(req, model, listRequiredFields) + if setupErr != nil { + return *setupErr, nil + } + return HandleList(&req, client, model), nil +} + +func setupRequest(req handler.Request, model *Model, requiredFields []string) (*util.MongoDBClient, *handler.ProgressEvent) { + util.SetupLogger("mongodb-atlas-backup-compliance-policy") + if modelValidation := validator.ValidateModel(requiredFields, model); modelValidation != nil { + return nil, modelValidation + } + util.SetDefaultProfileIfNotDefined(&model.Profile) + client, progressEventErr := util.NewAtlasClient(&req, model.Profile) + if progressEventErr != nil { + return nil, progressEventErr + } + return client, nil +} diff --git a/cfn-resources/backup-compliance-policy/cmd/resource/share.go b/cfn-resources/backup-compliance-policy/cmd/resource/share.go new file mode 100644 index 000000000..8d0e2dde6 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/cmd/resource/share.go @@ -0,0 +1,325 @@ +// Copyright 2026 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 resource + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" + "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + "go.mongodb.org/atlas-sdk/v20250312012/admin" + + "github.com/mongodb/mongodbatlas-cloudformation-resources/util" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/constants" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/logger" + progress_events "github.com/mongodb/mongodbatlas-cloudformation-resources/util/progressevent" +) + +const ( + policyStateActive = "ACTIVE" + errorCannotUpdateWithPendingAction = "CANNOT_UPDATE_BACKUP_COMPLIANCE_POLICY_SETTINGS_WITH_PENDING_ACTION" + errorCannotDisableBackupCompliancePolicy = "CANNOT_DISABLE_BACKUP_COMPLIANCE_POLICY" +) + +var callbackContext = map[string]any{"callbackBackupCompliancePolicy": true} + +func IsCallback(req *handler.Request) bool { + _, found := req.CallbackContext["callbackBackupCompliancePolicy"] + return found +} + +func isPendingActionError(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), errorCannotUpdateWithPendingAction) +} + +func handlePendingAction(ctx context.Context, client *util.MongoDBClient, model *Model, projectID string) handler.ProgressEvent { + policy, _, getErr := client.AtlasSDK.CloudBackupsApi.GetCompliancePolicy(ctx, projectID).Execute() + if getErr == nil && policy != nil { + SetBackupCompliancePolicyData(model, policy) + } + return inProgressEvent(model, policy) +} + +func checkPolicyNotFound(policy *admin.DataProtectionSettings20231001, apiResp *http.Response, err error, projectID string, operation constants.CfnFunctions) *handler.ProgressEvent { + if err != nil { + if apiResp != nil && apiResp.StatusCode == http.StatusNotFound { + pe := progress_events.GetFailedEventByCode( + "Backup Compliance Policy not found for project: "+projectID, + string(types.HandlerErrorCodeNotFound)) + return &pe + } + pe := handleError(apiResp, operation, err) + return &pe + } + + if policy != nil && policy.GetProjectId() == "" { + pe := progress_events.GetFailedEventByCode( + "Backup Compliance Policy not found for project: "+projectID, + string(types.HandlerErrorCodeNotFound)) + return &pe + } + + return nil +} + +func HandleCreate(req *handler.Request, client *util.MongoDBClient, model *Model) handler.ProgressEvent { + if IsCallback(req) { + return validateProgress(client, model, false) + } + + ctx := context.Background() + projectID := *model.ProjectId + + existingPolicy, apiResp, err := client.AtlasSDK.CloudBackupsApi.GetCompliancePolicy(ctx, projectID).Execute() + if err != nil { + if apiResp == nil || apiResp.StatusCode != http.StatusNotFound { + return handleError(apiResp, constants.CREATE, err) + } + } else if existingPolicy != nil && existingPolicy.GetState() == policyStateActive { + return handler.ProgressEvent{ + OperationStatus: handler.Failed, + Message: "Backup Compliance Policy already exists for project: " + projectID, + HandlerErrorCode: string(types.HandlerErrorCodeAlreadyExists), + } + } + + dataProtectionSettings := ExpandDataProtectionSettings(model, projectID) + + params := admin.UpdateCompliancePolicyApiParams{ + GroupId: projectID, + DataProtectionSettings20231001: dataProtectionSettings, + OverwriteBackupPolicies: util.Pointer(false), + } + + _, apiResp, err = client.AtlasSDK.CloudBackupsApi.UpdateCompliancePolicyWithParams(ctx, ¶ms).Execute() + if err != nil { + if isPendingActionError(err) { + return handlePendingAction(ctx, client, model, projectID) + } + return handleError(apiResp, constants.CREATE, err) + } + + policy, apiResp, err := client.AtlasSDK.CloudBackupsApi.GetCompliancePolicy(ctx, projectID).Execute() + if err != nil { + return handleError(apiResp, constants.CREATE, err) + } + + return inProgressEvent(model, policy) +} + +func HandleRead(req *handler.Request, client *util.MongoDBClient, model *Model) handler.ProgressEvent { + ctx := context.Background() + projectID := *model.ProjectId + + policy, apiResp, err := client.AtlasSDK.CloudBackupsApi.GetCompliancePolicy(ctx, projectID).Execute() + if pe := checkPolicyNotFound(policy, apiResp, err, projectID, constants.READ); pe != nil { + return *pe + } + + SetBackupCompliancePolicyData(model, policy) + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.ReadComplete, + ResourceModel: model, + } +} + +func HandleUpdate(req *handler.Request, client *util.MongoDBClient, model *Model) handler.ProgressEvent { + if IsCallback(req) { + return validateProgress(client, model, false) + } + + ctx := context.Background() + projectID := *model.ProjectId + + policy, apiResp, err := client.AtlasSDK.CloudBackupsApi.GetCompliancePolicy(ctx, projectID).Execute() + if pe := checkPolicyNotFound(policy, apiResp, err, projectID, constants.UPDATE); pe != nil { + return *pe + } + + dataProtectionSettings := ExpandDataProtectionSettings(model, projectID) + + params := admin.UpdateCompliancePolicyApiParams{ + GroupId: projectID, + DataProtectionSettings20231001: dataProtectionSettings, + OverwriteBackupPolicies: util.Pointer(false), + } + + _, apiResp, err = client.AtlasSDK.CloudBackupsApi.UpdateCompliancePolicyWithParams(ctx, ¶ms).Execute() + if err != nil { + return handleError(apiResp, constants.UPDATE, err) + } + + policy, apiResp, err = client.AtlasSDK.CloudBackupsApi.GetCompliancePolicy(ctx, projectID).Execute() + if err != nil { + return handleError(apiResp, constants.UPDATE, err) + } + + return inProgressEvent(model, policy) +} + +func HandleDelete(req *handler.Request, client *util.MongoDBClient, model *Model) handler.ProgressEvent { + if IsCallback(req) { + return validateProgress(client, model, true) + } + + ctx := context.Background() + projectID := *model.ProjectId + + policy, apiResp, err := client.AtlasSDK.CloudBackupsApi.GetCompliancePolicy(ctx, projectID).Execute() + if pe := checkPolicyNotFound(policy, apiResp, err, projectID, constants.DELETE); pe != nil { + return *pe + } + + apiResp, err = client.AtlasSDK.CloudBackupsApi.DisableCompliancePolicy(ctx, projectID).Execute() + if err == nil { + return inProgressEvent(model, nil) + } + + errorMessage := err.Error() + + if strings.Contains(errorMessage, errorCannotDisableBackupCompliancePolicy) { + _, _ = logger.Warnf("Cannot disable backup compliance policy for project %s: Policy requires MongoDB support or removing all clusters & retained snapshots", projectID) + return handler.ProgressEvent{ + OperationStatus: handler.Failed, + Message: fmt.Sprintf("Cannot disable Backup Compliance Policy for project %s. Disabling BCP requires MongoDB support or removing all clusters & retained snapshots. This can only be done by filing a support ticket.", projectID), + HandlerErrorCode: string(types.HandlerErrorCodeInvalidRequest), + } + } + + if isPendingActionError(err) { + return handlePendingAction(ctx, client, model, projectID) + } + + if apiResp != nil && apiResp.StatusCode == http.StatusNotFound { + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.Complete, + } + } + + _, _ = logger.Warnf("Unexpected delete error for project %s: %s", projectID, errorMessage) + return handleError(apiResp, constants.DELETE, err) +} + +func HandleList(req *handler.Request, client *util.MongoDBClient, model *Model) handler.ProgressEvent { + ctx := context.Background() + projectID := *model.ProjectId + + policy, apiResp, err := client.AtlasSDK.CloudBackupsApi.GetCompliancePolicy(ctx, projectID).Execute() + if err != nil { + if apiResp == nil || apiResp.StatusCode != http.StatusNotFound { + return handleError(apiResp, constants.LIST, err) + } + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.Complete, + ResourceModels: []interface{}{}, + } + } + + if policy != nil && policy.GetProjectId() == "" { + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.Complete, + ResourceModels: []interface{}{}, + } + } + + listModel := &Model{ProjectId: &projectID} + SetBackupCompliancePolicyData(listModel, policy) + model = listModel + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.Complete, + ResourceModels: []interface{}{model}, + } +} + +func inProgressEvent(model *Model, policy *admin.DataProtectionSettings20231001) handler.ProgressEvent { + if policy != nil { + SetBackupCompliancePolicyData(model, policy) + } + return handler.ProgressEvent{ + OperationStatus: handler.InProgress, + Message: constants.Pending, + ResourceModel: model, + CallbackDelaySeconds: callBackSeconds, + CallbackContext: callbackContext, + } +} + +func validateProgress(client *util.MongoDBClient, model *Model, isDelete bool) handler.ProgressEvent { + ctx := context.Background() + projectID := *model.ProjectId + + policy, resp, err := client.AtlasSDK.CloudBackupsApi.GetCompliancePolicy(ctx, projectID).Execute() + notFound := resp != nil && resp.StatusCode == http.StatusNotFound + policyDeleted := notFound || (policy != nil && policy.GetProjectId() == "") + + if err != nil && !notFound { + if isPendingActionError(err) { + return inProgressEvent(model, policy) + } + return handleError(resp, constants.CREATE, err) + } + + if isDelete && policyDeleted { + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.Complete, + } + } + + if isDelete { + // Don't call DisableCompliancePolicy again - it always returns 204 + return inProgressEvent(model, nil) + } + + if policy != nil { + SetBackupCompliancePolicyData(model, policy) + if policy.GetState() == policyStateActive { + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.Complete, + ResourceModel: model, + } + } + return inProgressEvent(model, policy) + } + + if notFound { + return handler.ProgressEvent{ + OperationStatus: handler.Failed, + Message: "Backup Compliance Policy was deleted during operation", + HandlerErrorCode: string(types.HandlerErrorCodeNotFound), + } + } + + return inProgressEvent(model, policy) +} + +func handleError(response *http.Response, method constants.CfnFunctions, err error) handler.ProgressEvent { + errMsg := fmt.Sprintf("%s error:%s", method, err.Error()) + return progress_events.GetFailedEventByResponse(errMsg, response) +} diff --git a/cfn-resources/backup-compliance-policy/docs/README.md b/cfn-resources/backup-compliance-policy/docs/README.md new file mode 100644 index 000000000..50b2576e0 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/docs/README.md @@ -0,0 +1,280 @@ +# MongoDB::Atlas::BackupCompliancePolicy + +Resource for managing MongoDB Atlas Backup Compliance Policy. Backup Compliance Policy prevents any user, regardless of role, from modifying or deleting specific cluster settings, backups, and backup configurations. When enabled, the Backup Compliance Policy will be applied as the minimum policy for all clusters and backups in the project. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "MongoDB::Atlas::BackupCompliancePolicy",
+    "Properties" : {
+        "Profile" : String,
+        "ProjectId" : String,
+        "AuthorizedEmail" : String,
+        "AuthorizedUserFirstName" : String,
+        "AuthorizedUserLastName" : String,
+        "CopyProtectionEnabled" : Boolean,
+        "EncryptionAtRestEnabled" : Boolean,
+        "RestoreWindowDays" : Integer,
+        "OnDemandPolicyItem" : OnDemandPolicyItem,
+        "PitEnabled" : Boolean,
+        "PolicyItemHourly" : ScheduledPolicyItem,
+        "PolicyItemDaily" : ScheduledPolicyItem,
+        "PolicyItemWeekly" : [ ScheduledPolicyItem, ... ],
+        "PolicyItemMonthly" : [ ScheduledPolicyItem, ... ],
+        "PolicyItemYearly" : [ ScheduledPolicyItem, ... ]
+    }
+}
+
+ +### YAML + +
+Type: MongoDB::Atlas::BackupCompliancePolicy
+Properties:
+    Profile: String
+    ProjectId: String
+    AuthorizedEmail: String
+    AuthorizedUserFirstName: String
+    AuthorizedUserLastName: String
+    CopyProtectionEnabled: Boolean
+    EncryptionAtRestEnabled: Boolean
+    RestoreWindowDays: Integer
+    OnDemandPolicyItem: OnDemandPolicyItem
+    PitEnabled: Boolean
+    PolicyItemHourly: ScheduledPolicyItem
+    PolicyItemDaily: ScheduledPolicyItem
+    PolicyItemWeekly: 
+      - ScheduledPolicyItem
+    PolicyItemMonthly: 
+      - ScheduledPolicyItem
+    PolicyItemYearly: 
+      - ScheduledPolicyItem
+
+ +## Properties + +#### Profile + +Profile used to provide credentials information, (a secret with the cfn/atlas/profile/{Profile}, is required), if not provided default is used + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### ProjectId + +The unique identifier of the project for the Backup Compliance Policy. + +_Required_: Yes + +_Type_: String + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### AuthorizedEmail + +Email address of the user authorized to update the Backup Compliance Policy settings. + +_Required_: Yes + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### AuthorizedUserFirstName + +First name of the user authorized to update the Backup Compliance Policy settings. + +_Required_: Yes + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### AuthorizedUserLastName + +Last name of the user authorized to update the Backup Compliance Policy settings. + +_Required_: Yes + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### CopyProtectionEnabled + +Flag that indicates whether to enable additional copy protection for the cluster. If enabled, cloud backup snapshots cannot be deleted until the retention period expires. Defaults to false if not specified. + +_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) + +#### EncryptionAtRestEnabled + +Flag that indicates whether Encryption at Rest using Customer Key Management is required for all clusters with a Backup Compliance Policy. Defaults to false if not specified. + +_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) + +#### RestoreWindowDays + +Number of days back in time you can restore to with Continuous Cloud Backup accuracy. Must be a positive, non-zero integer. This field is optional and computed from the API. + +_Required_: No + +_Type_: Integer + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### OnDemandPolicyItem + +On-demand backup policy item configuration. When provided, FrequencyInterval, RetentionUnit, and RetentionValue are required. + +_Required_: No + +_Type_: OnDemandPolicyItem + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### PitEnabled + +Flag that indicates whether the cluster uses Continuous Cloud Backup. If enabled, cloud backup snapshots are taken every 6 hours. Defaults to false if not specified. + +_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) + +#### PolicyItemHourly + +Scheduled backup policy item configuration (hourly, daily, weekly, monthly, or yearly). When provided, FrequencyInterval, RetentionUnit, and RetentionValue are required. + +_Required_: No + +_Type_: ScheduledPolicyItem + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### PolicyItemDaily + +_Required_: No + +_Type_: ScheduledPolicyItem + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### PolicyItemWeekly + +Weekly backup policy item configuration. Multiple weekly policy items are allowed. + +_Required_: No + +_Type_: List of ScheduledPolicyItem + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### PolicyItemMonthly + +Monthly backup policy item configuration. Multiple monthly policy items are allowed. + +_Required_: No + +_Type_: List of ScheduledPolicyItem + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### PolicyItemYearly + +Yearly backup policy item configuration. Multiple yearly policy items are allowed. + +_Required_: No + +_Type_: List of ScheduledPolicyItem + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +## Return Values + +### Ref + +When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the ProjectId. + +### Fn::GetAtt + +The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values. + +For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html). + +#### State + +Current state of the Backup Compliance Policy settings. Possible values are: ACTIVE, INACTIVE. + +#### UpdatedDate + +ISO 8601 timestamp in UTC of when the Backup Compliance Policy settings were last updated. + +#### UpdatedUser + +Email address of the user who last updated the Backup Compliance Policy settings. + +#### Id + +Returns the Id value. + +#### FrequencyType + +Returns the FrequencyType value. + +#### Id + +Returns the Id value. + +#### FrequencyType + +Returns the FrequencyType value. + +#### Id + +Returns the Id value. + +#### FrequencyType + +Returns the FrequencyType value. + +#### Id + +Returns the Id value. + +#### FrequencyType + +Returns the FrequencyType value. + +#### Id + +Returns the Id value. + +#### FrequencyType + +Returns the FrequencyType value. + +#### Id + +Returns the Id value. + +#### FrequencyType + +Returns the FrequencyType value. + diff --git a/cfn-resources/backup-compliance-policy/docs/ondemandpolicyitem.md b/cfn-resources/backup-compliance-policy/docs/ondemandpolicyitem.md new file mode 100644 index 000000000..50d133302 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/docs/ondemandpolicyitem.md @@ -0,0 +1,60 @@ +# MongoDB::Atlas::BackupCompliancePolicy OnDemandPolicyItem + +On-demand backup policy item configuration. When provided, FrequencyInterval, RetentionUnit, and RetentionValue are required. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "FrequencyInterval" : Integer,
+    "RetentionUnit" : String,
+    "RetentionValue" : Integer
+}
+
+ +### YAML + +
+FrequencyInterval: Integer
+RetentionUnit: String
+RetentionValue: Integer
+
+ +## Properties + +#### FrequencyInterval + +Number that indicates the frequency interval for a set of snapshots. Required when OnDemandPolicyItem is provided. + +_Required_: No + +_Type_: Integer + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### RetentionUnit + +Unit of time in which MongoDB Cloud measures snapshot retention. Required when OnDemandPolicyItem is provided. + +_Required_: No + +_Type_: String + +_Allowed Values_: days | weeks | months | years + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### RetentionValue + +Duration in days, weeks, months, or years that MongoDB Cloud retains the snapshot. Required when OnDemandPolicyItem is provided. + +_Required_: No + +_Type_: Integer + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + diff --git a/cfn-resources/backup-compliance-policy/docs/scheduledpolicyitem.md b/cfn-resources/backup-compliance-policy/docs/scheduledpolicyitem.md new file mode 100644 index 000000000..ec61d78be --- /dev/null +++ b/cfn-resources/backup-compliance-policy/docs/scheduledpolicyitem.md @@ -0,0 +1,86 @@ +# MongoDB::Atlas::BackupCompliancePolicy ScheduledPolicyItem + +Scheduled backup policy item configuration (hourly, daily, weekly, monthly, or yearly). When provided, FrequencyInterval, RetentionUnit, and RetentionValue are required. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Id" : String,
+    "FrequencyType" : String,
+    "FrequencyInterval" : Integer,
+    "RetentionUnit" : String,
+    "RetentionValue" : Integer
+}
+
+ +### YAML + +
+Id: String
+FrequencyType: String
+FrequencyInterval: Integer
+RetentionUnit: String
+RetentionValue: Integer
+
+ +## Properties + +#### Id + +Unique identifier of the backup policy item. + +_Required_: No + +_Type_: String + +_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) + +#### FrequencyType + +Frequency associated with the backup policy item. One of the following values: hourly, daily, weekly, monthly, or yearly. This is a read-only value. + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### FrequencyInterval + +Desired frequency of the new backup policy item specified by frequencyType. Required when the policy item is provided. + +_Required_: No + +_Type_: Integer + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### RetentionUnit + +Unit of time in which MongoDB Cloud measures snapshot retention. Required when the policy item is provided. + +_Required_: No + +_Type_: String + +_Allowed Values_: days | weeks | months | years + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### RetentionValue + +Duration in days, weeks, months, or years that MongoDB Cloud retains the snapshot. Required when the policy item is provided. For less frequent policy items, MongoDB Cloud requires that you specify a value greater than or equal to the value specified for more frequent policy items. + +_Required_: No + +_Type_: Integer + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + diff --git a/cfn-resources/backup-compliance-policy/mongodb-atlas-backupcompliancepolicy.json b/cfn-resources/backup-compliance-policy/mongodb-atlas-backupcompliancepolicy.json new file mode 100644 index 000000000..a3d452359 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/mongodb-atlas-backupcompliancepolicy.json @@ -0,0 +1,203 @@ +{ + "typeName": "MongoDB::Atlas::BackupCompliancePolicy", + "description": "Resource for managing MongoDB Atlas Backup Compliance Policy. Backup Compliance Policy prevents any user, regardless of role, from modifying or deleting specific cluster settings, backups, and backup configurations. When enabled, the Backup Compliance Policy will be applied as the minimum policy for all clusters and backups in the project.", + "definitions": { + "OnDemandPolicyItem": { + "type": "object", + "description": "On-demand backup policy item configuration. When provided, FrequencyInterval, RetentionUnit, and RetentionValue are required.", + "properties": { + "Id": { + "description": "Unique identifier of the backup policy item.", + "type": "string", + "pattern": "^([a-f0-9]{24})$" + }, + "FrequencyInterval": { + "description": "Number that indicates the frequency interval for a set of snapshots. Required when OnDemandPolicyItem is provided.", + "type": "integer" + }, + "FrequencyType": { + "description": "Frequency associated with the backup policy item. For on-demand policies, the frequency type is 'ondemand'. This is a read-only value.", + "type": "string" + }, + "RetentionUnit": { + "description": "Unit of time in which MongoDB Cloud measures snapshot retention. Required when OnDemandPolicyItem is provided.", + "type": "string", + "enum": ["days", "weeks", "months", "years"] + }, + "RetentionValue": { + "description": "Duration in days, weeks, months, or years that MongoDB Cloud retains the snapshot. Required when OnDemandPolicyItem is provided.", + "type": "integer" + } + }, + "additionalProperties": false + }, + "ScheduledPolicyItem": { + "type": "object", + "description": "Scheduled backup policy item configuration (hourly, daily, weekly, monthly, or yearly). When provided, FrequencyInterval, RetentionUnit, and RetentionValue are required.", + "properties": { + "Id": { + "description": "Unique identifier of the backup policy item.", + "type": "string", + "pattern": "^([a-f0-9]{24})$" + }, + "FrequencyType": { + "description": "Frequency associated with the backup policy item. One of the following values: hourly, daily, weekly, monthly, or yearly. This is a read-only value.", + "type": "string" + }, + "FrequencyInterval": { + "description": "Desired frequency of the new backup policy item specified by frequencyType. Required when the policy item is provided.", + "type": "integer" + }, + "RetentionUnit": { + "description": "Unit of time in which MongoDB Cloud measures snapshot retention. Required when the policy item is provided.", + "type": "string", + "enum": ["days", "weeks", "months", "years"] + }, + "RetentionValue": { + "description": "Duration in days, weeks, months, or years that MongoDB Cloud retains the snapshot. Required when the policy item is provided. For less frequent policy items, MongoDB Cloud requires that you specify a value greater than or equal to the value specified for more frequent policy items.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "properties": { + "Profile": { + "description": "Profile used to provide credentials information, (a secret with the cfn/atlas/profile/{Profile}, is required), if not provided default is used", + "type": "string", + "default": "default" + }, + "ProjectId": { + "description": "The unique identifier of the project for the Backup Compliance Policy.", + "type": "string" + }, + "AuthorizedEmail": { + "description": "Email address of the user authorized to update the Backup Compliance Policy settings.", + "type": "string" + }, + "AuthorizedUserFirstName": { + "description": "First name of the user authorized to update the Backup Compliance Policy settings.", + "type": "string" + }, + "AuthorizedUserLastName": { + "description": "Last name of the user authorized to update the Backup Compliance Policy settings.", + "type": "string" + }, + "CopyProtectionEnabled": { + "description": "Flag that indicates whether to enable additional copy protection for the cluster. If enabled, cloud backup snapshots cannot be deleted until the retention period expires. Defaults to false if not specified.", + "type": "boolean" + }, + "EncryptionAtRestEnabled": { + "description": "Flag that indicates whether Encryption at Rest using Customer Key Management is required for all clusters with a Backup Compliance Policy. Defaults to false if not specified.", + "type": "boolean" + }, + "RestoreWindowDays": { + "description": "Number of days back in time you can restore to with Continuous Cloud Backup accuracy. Must be a positive, non-zero integer. This field is optional and computed from the API.", + "type": "integer" + }, + "OnDemandPolicyItem": { + "description": "On-demand backup policy item configuration.", + "$ref": "#/definitions/OnDemandPolicyItem", + "type": "object" + }, + "PitEnabled": { + "description": "Flag that indicates whether the cluster uses Continuous Cloud Backup. If enabled, cloud backup snapshots are taken every 6 hours. Defaults to false if not specified.", + "type": "boolean" + }, + "State": { + "description": "Current state of the Backup Compliance Policy settings. Possible values are: ACTIVE, INACTIVE.", + "type": "string" + }, + "UpdatedDate": { + "description": "ISO 8601 timestamp in UTC of when the Backup Compliance Policy settings were last updated.", + "type": "string", + "format": "date-time" + }, + "UpdatedUser": { + "description": "Email address of the user who last updated the Backup Compliance Policy settings.", + "type": "string" + }, + "PolicyItemHourly": { + "description": "Hourly backup policy item configuration. Only one hourly policy item is allowed.", + "$ref": "#/definitions/ScheduledPolicyItem", + "type": "object" + }, + "PolicyItemDaily": { + "description": "Daily backup policy item configuration. Only one daily policy item is allowed.", + "$ref": "#/definitions/ScheduledPolicyItem", + "type": "object" + }, + "PolicyItemWeekly": { + "description": "Weekly backup policy item configuration. Multiple weekly policy items are allowed.", + "type": "array", + "items": { + "$ref": "#/definitions/ScheduledPolicyItem" + }, + "insertionOrder": false + }, + "PolicyItemMonthly": { + "description": "Monthly backup policy item configuration. Multiple monthly policy items are allowed.", + "type": "array", + "items": { + "$ref": "#/definitions/ScheduledPolicyItem" + }, + "insertionOrder": false + }, + "PolicyItemYearly": { + "description": "Yearly backup policy item configuration. Multiple yearly policy items are allowed.", + "type": "array", + "items": { + "$ref": "#/definitions/ScheduledPolicyItem" + }, + "insertionOrder": false + } + }, + "additionalProperties": false, + "required": [ + "ProjectId", + "AuthorizedEmail", + "AuthorizedUserFirstName", + "AuthorizedUserLastName" + ], + "readOnlyProperties": [ + "/properties/State", + "/properties/UpdatedDate", + "/properties/UpdatedUser", + "/properties/OnDemandPolicyItem/Id", + "/properties/OnDemandPolicyItem/FrequencyType", + "/properties/PolicyItemHourly/Id", + "/properties/PolicyItemHourly/FrequencyType", + "/properties/PolicyItemDaily/Id", + "/properties/PolicyItemDaily/FrequencyType", + "/properties/PolicyItemWeekly/*/Id", + "/properties/PolicyItemWeekly/*/FrequencyType", + "/properties/PolicyItemMonthly/*/Id", + "/properties/PolicyItemMonthly/*/FrequencyType", + "/properties/PolicyItemYearly/*/Id", + "/properties/PolicyItemYearly/*/FrequencyType" + ], + "createOnlyProperties": ["/properties/ProjectId"], + "primaryIdentifier": ["/properties/ProjectId"], + "handlers": { + "create": { + "permissions": ["secretsmanager:GetSecretValue"] + }, + "read": { + "permissions": ["secretsmanager:GetSecretValue"] + }, + "update": { + "permissions": ["secretsmanager:GetSecretValue"] + }, + "delete": { + "permissions": ["secretsmanager:GetSecretValue"] + }, + "list": { + "permissions": ["secretsmanager:GetSecretValue"] + } + }, + "documentationUrl": "https://github.com/mongodb/mongodbatlas-cloudformation-resources/blob/master/cfn-resources/backup-compliance-policy/README.md", + "tagging": { + "taggable": false + }, + "sourceUrl": "https://github.com/mongodb/mongodbatlas-cloudformation-resources/tree/master/cfn-resources/backup-compliance-policy" +} diff --git a/cfn-resources/backup-compliance-policy/resource-role.yaml b/cfn-resources/backup-compliance-policy/resource-role.yaml new file mode 100644 index 000000000..546fb85f1 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/resource-role.yaml @@ -0,0 +1,38 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: > + This CloudFormation template creates a role assumed by CloudFormation + during CRUDL operations to mutate resources on behalf of the customer. + +Resources: + ExecutionRole: + Type: AWS::IAM::Role + Properties: + MaxSessionDuration: 8400 + AssumeRolePolicyDocument: + 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-BackupCompliancePolicy/* + Path: "/" + Policies: + - PolicyName: ResourceTypePolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - "secretsmanager:GetSecretValue" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/cfn-resources/backup-compliance-policy/template.yml b/cfn-resources/backup-compliance-policy/template.yml new file mode 100644 index 000000000..3329b174e --- /dev/null +++ b/cfn-resources/backup-compliance-policy/template.yml @@ -0,0 +1,27 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the MongoDB::Atlas::BackupCompliancePolicy resource type + +Globals: + Function: + Timeout: 180 # docker start-up times can be long for SAM CLI + MemorySize: 256 + +Resources: + TypeFunction: + Type: AWS::Serverless::Function + Properties: + Handler: bootstrap + Runtime: provided.al2 + CodeUri: bin/ + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: bootstrap + Runtime: provided.al2 + CodeUri: bin/ + Environment: + Variables: + MODE: Test + LOG_LEVEL: debug diff --git a/cfn-resources/backup-compliance-policy/test/backup-compliance-policy.sample-cfn-request.json b/cfn-resources/backup-compliance-policy/test/backup-compliance-policy.sample-cfn-request.json new file mode 100644 index 000000000..ab15bb805 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/test/backup-compliance-policy.sample-cfn-request.json @@ -0,0 +1,13 @@ +{ + "desiredResourceState": { + "ProjectId": "", + "AuthorizedEmail": "", + "AuthorizedUserFirstName": "", + "AuthorizedUserLastName": "", + "CopyProtectionEnabled": false, + "EncryptionAtRestEnabled": false, + "PitEnabled": false + }, + "providerLogGroupName": "mongodb-atlas-backup-compliance-policy-logs", + "previousResourceState": {} +} diff --git a/cfn-resources/backup-compliance-policy/test/cfn-test-create-inputs.sh b/cfn-resources/backup-compliance-policy/test/cfn-test-create-inputs.sh new file mode 100755 index 000000000..e6cfcdda8 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/test/cfn-test-create-inputs.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# cfn-test-create-inputs.sh +# +# This tool generates json files in the inputs/ for `cfn test`. +# + +set -o errexit +set -o nounset +set -o pipefail + +function usage { + echo "Creates test inputs for Backup Compliance Policy" + echo "Usage: $0 [authorized_email]" + echo "Creates a new project (cluster is not required - policy can be created without a cluster)" +} + +if [ "$#" -lt 1 ]; then usage; exit 1; fi +if [[ "$*" == help ]]; then usage; exit 0; fi + +rm -rf inputs +mkdir inputs + +#set profile +profile="default" +if [ ${MONGODB_ATLAS_PROFILE+x} ]; then + echo "profile set to ${MONGODB_ATLAS_PROFILE}" + profile=${MONGODB_ATLAS_PROFILE} +fi + +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 + +# Get the current user email for authorized email +# Second argument is optional - if not provided, use default test email +authorizedEmail="${2:-test@example.com}" + +WORDTOREMOVE="template." +cd "$(dirname "$0")" || exit +for inputFile in inputs_*; do + outputFile=${inputFile//$WORDTOREMOVE/} + jq --arg projectId "$projectId" \ + --arg authorizedEmail "$authorizedEmail" \ + --arg profile "$profile" \ + '.Profile?|=$profile | .ProjectId?|=$projectId |.AuthorizedEmail?|=$authorizedEmail' \ + "$inputFile" >"../inputs/$outputFile" +done +cd .. + +ls -l inputs diff --git a/cfn-resources/backup-compliance-policy/test/cfn-test-delete-inputs.sh b/cfn-resources/backup-compliance-policy/test/cfn-test-delete-inputs.sh new file mode 100755 index 000000000..e53aeb41d --- /dev/null +++ b/cfn-resources/backup-compliance-policy/test/cfn-test-delete-inputs.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# cfn-test-delete-inputs.sh +# +# Needs to exist to be called in Publish, but no cleanup is needed. diff --git a/cfn-resources/backup-compliance-policy/test/contract-testing/cfn-test-create.sh b/cfn-resources/backup-compliance-policy/test/contract-testing/cfn-test-create.sh new file mode 100755 index 000000000..076e58cc2 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/test/contract-testing/cfn-test-create.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Run this script with the Makefile +# make create-test-resources +# +# This tool generates json files in the inputs/ for `cfn test`. +# +set -o errexit +set -o nounset +set -o pipefail + +projectName="cfn-test-bot-$(date +%s)-$RANDOM" + +# create project +projectId=$(atlas projects create "${projectName}" --output=json | jq -r '.id') + +echo "projectId: $projectId" +echo "projectName: $projectName" + +# Get the current user email for authorized email +# Use default test email for contract testing +authorizedEmail="test@example.com" + +./test/cfn-test-create-inputs.sh "$projectName" "$authorizedEmail" diff --git a/cfn-resources/backup-compliance-policy/test/contract-testing/cfn-test-delete.sh b/cfn-resources/backup-compliance-policy/test/contract-testing/cfn-test-delete.sh new file mode 100755 index 000000000..71286ddfb --- /dev/null +++ b/cfn-resources/backup-compliance-policy/test/contract-testing/cfn-test-delete.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# This tool deletes the mongodb resources used for `cfn test` as inputs. +set -o errexit +set -o nounset +set -o pipefail + +projectId=$(jq -r '.ProjectId' ./inputs/inputs_1_create.json) + +# 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/backup-compliance-policy/test/inputs_1_create.template.json b/cfn-resources/backup-compliance-policy/test/inputs_1_create.template.json new file mode 100644 index 000000000..01b34f1ce --- /dev/null +++ b/cfn-resources/backup-compliance-policy/test/inputs_1_create.template.json @@ -0,0 +1,7 @@ +{ + "ProjectId": "", + "AuthorizedEmail": "", + "AuthorizedUserFirstName": "Jane", + "AuthorizedUserLastName": "Smith", + "RestoreWindowDays": "7" +} diff --git a/cfn-resources/backup-compliance-policy/test/inputs_1_update.template.json b/cfn-resources/backup-compliance-policy/test/inputs_1_update.template.json new file mode 100644 index 000000000..3fe608906 --- /dev/null +++ b/cfn-resources/backup-compliance-policy/test/inputs_1_update.template.json @@ -0,0 +1,8 @@ +{ + "ProjectId": "", + "AuthorizedEmail": "", + "AuthorizedUserFirstName": "Jane", + "AuthorizedUserLastName": "Smith", + "RestoreWindowDays": "14", + "CopyProtectionEnabled": "true" +} diff --git a/cfn-resources/util/constants/constants.go b/cfn-resources/util/constants/constants.go index 5481774ca..f0814ffec 100644 --- a/cfn-resources/util/constants/constants.go +++ b/cfn-resources/util/constants/constants.go @@ -162,4 +162,8 @@ const ( ProcessorName = "ProcessorName" Pipeline = "Pipeline" WorkspaceName = "WorkspaceName" + + AuthorizedEmail = "AuthorizedEmail" + AuthorizedUserFirstName = "AuthorizedUserFirstName" + AuthorizedUserLastName = "AuthorizedUserLastName" ) diff --git a/examples/backup-compliance-policy/README.md b/examples/backup-compliance-policy/README.md new file mode 100644 index 000000000..ddf09c52f --- /dev/null +++ b/examples/backup-compliance-policy/README.md @@ -0,0 +1,25 @@ +# How to create a MongoDB::Atlas::BackupCompliancePolicy + +## Step 1: Activate the backup compliance policy resource in cloudformation + Step a: Create Role using [execution-role.yaml](https://github.com/mongodb/mongodbatlas-cloudformation-resources/blob/master/examples/execution-role.yaml) in CFN resources folder. + + Step b: Search for Mongodb::Atlas::BackupCompliancePolicy resource. + + (CloudFormation > Public extensions > choose 'Third party' > Search with " Execution name prefix = MongoDB " ) + Step c: Select and activate + Enter the RoleArn that is created in step 1. + + Your BackupCompliancePolicy Resource is ready to use. + +## Step 2: Create template using [backup-compliance-policy.json](backup-compliance-policy.json) or [backup-compliance-policy-simple.json](backup-compliance-policy-simple.json) + Note: Make sure you are providing appropriate values for: + 1. ProjectId + 2. AuthorizedEmail + 3. AuthorizedUserFirstName + 4. AuthorizedUserLastName + 5. Profile (optional) + 6. CopyProtectionEnabled (optional) + 7. EncryptionAtRestEnabled (optional) + 8. PitEnabled (optional) + 9. RestoreWindowDays (optional) + 10. Policy items (Hourly, Daily, Weekly, Monthly, Yearly) (optional) diff --git a/examples/backup-compliance-policy/backup-compliance-policy.json b/examples/backup-compliance-policy/backup-compliance-policy.json new file mode 100644 index 000000000..5dc24ebde --- /dev/null +++ b/examples/backup-compliance-policy/backup-compliance-policy.json @@ -0,0 +1,165 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "This template creates a Backup Compliance Policy on the MongoDB Atlas API, this will be billed to your Atlas account.", + "Parameters": { + "ProjectId": { + "Type": "String", + "Description": "Unique 24-hexadecimal digit string that identifies your project" + }, + "AuthorizedEmail": { + "Type": "String", + "Description": "Email address of the user authorized to update the Backup Compliance Policy settings" + }, + "AuthorizedUserFirstName": { + "Type": "String", + "Description": "First name of the user authorized to update the Backup Compliance Policy settings" + }, + "AuthorizedUserLastName": { + "Type": "String", + "Description": "Last name of the user authorized to update the Backup Compliance Policy settings" + }, + "Profile": { + "Type": "String", + "Description": "Secret Manager Profile that contains the Atlas Programmatic keys", + "Default": "default" + }, + "CopyProtectionEnabled": { + "Type": "String", + "Description": "Flag that indicates whether to enable additional copy protection for the cluster", + "Default": "false", + "AllowedValues": ["true", "false"] + }, + "EncryptionAtRestEnabled": { + "Type": "String", + "Description": "Flag that indicates whether Encryption at Rest using Customer Key Management is required", + "Default": "false", + "AllowedValues": ["true", "false"] + }, + "PitEnabled": { + "Type": "String", + "Description": "Flag that indicates whether the cluster uses Continuous Cloud Backup", + "Default": "false", + "AllowedValues": ["true", "false"] + }, + "RestoreWindowDays": { + "Type": "Number", + "Description": "Number of days back in time you can restore to with Continuous Cloud Backup accuracy", + "Default": 7 + } + }, + "Conditions": { + "IsCopyProtectionEnabled": { + "Fn::Equals": [ + { + "Ref": "CopyProtectionEnabled" + }, + "true" + ] + }, + "IsEncryptionAtRestEnabled": { + "Fn::Equals": [ + { + "Ref": "EncryptionAtRestEnabled" + }, + "true" + ] + }, + "IsPitEnabled": { + "Fn::Equals": [ + { + "Ref": "PitEnabled" + }, + "true" + ] + } + }, + "Resources": { + "BackupCompliancePolicy": { + "Type": "MongoDB::Atlas::BackupCompliancePolicy", + "Properties": { + "Profile": { + "Ref": "Profile" + }, + "ProjectId": { + "Ref": "ProjectId" + }, + "AuthorizedEmail": { + "Ref": "AuthorizedEmail" + }, + "AuthorizedUserFirstName": { + "Ref": "AuthorizedUserFirstName" + }, + "AuthorizedUserLastName": { + "Ref": "AuthorizedUserLastName" + }, + "CopyProtectionEnabled": { + "Fn::If": ["IsCopyProtectionEnabled", true, false] + }, + "EncryptionAtRestEnabled": { + "Fn::If": ["IsEncryptionAtRestEnabled", true, false] + }, + "PitEnabled": { + "Fn::If": ["IsPitEnabled", true, false] + }, + "RestoreWindowDays": { + "Ref": "RestoreWindowDays" + }, + "PolicyItemHourly": { + "FrequencyInterval": 6, + "RetentionUnit": "days", + "RetentionValue": 7 + }, + "PolicyItemDaily": { + "FrequencyInterval": 1, + "RetentionUnit": "days", + "RetentionValue": 14 + }, + "PolicyItemWeekly": [ + { + "FrequencyInterval": 1, + "RetentionUnit": "weeks", + "RetentionValue": 4 + } + ], + "PolicyItemMonthly": [ + { + "FrequencyInterval": 1, + "RetentionUnit": "months", + "RetentionValue": 12 + } + ] + } + } + }, + "Outputs": { + "PolicyProjectId": { + "Description": "Unique identifier of the project for the Backup Compliance Policy", + "Export": { + "Name": { + "Fn::Sub": "${AWS::StackName}-ProjectId" + } + }, + "Value": { + "Ref": "BackupCompliancePolicy" + } + }, + "PolicyState": { + "Description": "Current state of the Backup Compliance Policy settings", + "Value": { + "Fn::GetAtt": ["BackupCompliancePolicy", "State"] + } + }, + "UpdatedDate": { + "Description": "Date and time when the Backup Compliance Policy settings were last updated", + "Value": { + "Fn::GetAtt": ["BackupCompliancePolicy", "UpdatedDate"] + } + }, + "UpdatedUser": { + "Description": "Email address of the user who last updated the Backup Compliance Policy settings", + "Value": { + "Fn::GetAtt": ["BackupCompliancePolicy", "UpdatedUser"] + } + } + } +} From 938d9f58d4d71fa5b53bd7dae239dbdb7e5548d8 Mon Sep 17 00:00:00 2001 From: Rakhul S Prakash Date: Tue, 20 Jan 2026 10:50:16 +0530 Subject: [PATCH 2/5] Add contract testing workflow, use http util functions --- .github/workflows/contract-testing.yaml | 46 ++++++++++++++++++- .../backup-compliance-policy/Makefile | 6 ++- .../cmd/resource/{share.go => handlers.go} | 10 ++-- 3 files changed, 55 insertions(+), 7 deletions(-) rename cfn-resources/backup-compliance-policy/cmd/resource/{share.go => handlers.go} (96%) diff --git a/.github/workflows/contract-testing.yaml b/.github/workflows/contract-testing.yaml index 21c4f122d..ab660c710 100644 --- a/.github/workflows/contract-testing.yaml +++ b/.github/workflows/contract-testing.yaml @@ -15,6 +15,7 @@ jobs: alert-configuration: ${{ steps.filter.outputs.alert-configuration }} api-key: ${{ steps.filter.outputs.api-key }} auditing: ${{ steps.filter.outputs.auditing }} + backup-compliance-policy: ${{ steps.filter.outputs.backup-compliance-policy }} cloud-backup-restore-jobs: ${{ steps.filter.outputs.cloud-backup-restore-jobs }} cluster-outage-simulation: ${{ steps.filter.outputs.cluster-outage-simulation }} federated-database-instance: ${{ steps.filter.outputs.federated-database-instance }} @@ -48,6 +49,8 @@ jobs: - 'cfn-resources/api-key/**' auditing: - 'cfn-resources/auditing/**' + backup-compliance-policy: + - 'cfn-resources/backup-compliance-policy/**' cloud-backup-restore-jobs: - 'cfn-resources/cloud-backup-restore-jobs/**' cluster-outage-simulation: @@ -243,7 +246,48 @@ jobs: cat inputs/inputs_1_update.json cat inputs/inputs_2_create.json cat inputs/inputs_2_update.json - + + make run-contract-testing + make delete-test-resources + backup-compliance-policy: + needs: change-detection + if: ${{ needs.change-detection.outputs.backup-compliance-policy == 'true' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + - 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@c2a20b1822cc4a6bc594ff7f1dbb658758e383c3 + with: + use-installer: true + - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 + 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@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 + 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: | + cd cfn-resources/backup-compliance-policy + make create-test-resources + + cat inputs/inputs_1_create.json + cat inputs/inputs_1_update.json + make run-contract-testing make delete-test-resources cloud-backup-restore-jobs: diff --git a/cfn-resources/backup-compliance-policy/Makefile b/cfn-resources/backup-compliance-policy/Makefile index 56b0b3c6f..f0e934f54 100644 --- a/cfn-resources/backup-compliance-policy/Makefile +++ b/cfn-resources/backup-compliance-policy/Makefile @@ -20,7 +20,11 @@ clean: create-test-resources: @echo "==> Creating test files for contract testing" - ./test/contract-testing/cfn-test-create-inputs.sh + ./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" diff --git a/cfn-resources/backup-compliance-policy/cmd/resource/share.go b/cfn-resources/backup-compliance-policy/cmd/resource/handlers.go similarity index 96% rename from cfn-resources/backup-compliance-policy/cmd/resource/share.go rename to cfn-resources/backup-compliance-policy/cmd/resource/handlers.go index 8d0e2dde6..a33555827 100644 --- a/cfn-resources/backup-compliance-policy/cmd/resource/share.go +++ b/cfn-resources/backup-compliance-policy/cmd/resource/handlers.go @@ -60,7 +60,7 @@ func handlePendingAction(ctx context.Context, client *util.MongoDBClient, model func checkPolicyNotFound(policy *admin.DataProtectionSettings20231001, apiResp *http.Response, err error, projectID string, operation constants.CfnFunctions) *handler.ProgressEvent { if err != nil { - if apiResp != nil && apiResp.StatusCode == http.StatusNotFound { + if util.StatusNotFound(apiResp) { pe := progress_events.GetFailedEventByCode( "Backup Compliance Policy not found for project: "+projectID, string(types.HandlerErrorCodeNotFound)) @@ -90,7 +90,7 @@ func HandleCreate(req *handler.Request, client *util.MongoDBClient, model *Model existingPolicy, apiResp, err := client.AtlasSDK.CloudBackupsApi.GetCompliancePolicy(ctx, projectID).Execute() if err != nil { - if apiResp == nil || apiResp.StatusCode != http.StatusNotFound { + if !util.StatusNotFound(apiResp) { return handleError(apiResp, constants.CREATE, err) } } else if existingPolicy != nil && existingPolicy.GetState() == policyStateActive { @@ -210,7 +210,7 @@ func HandleDelete(req *handler.Request, client *util.MongoDBClient, model *Model return handlePendingAction(ctx, client, model, projectID) } - if apiResp != nil && apiResp.StatusCode == http.StatusNotFound { + if util.StatusNotFound(apiResp) { return handler.ProgressEvent{ OperationStatus: handler.Success, Message: constants.Complete, @@ -227,7 +227,7 @@ func HandleList(req *handler.Request, client *util.MongoDBClient, model *Model) policy, apiResp, err := client.AtlasSDK.CloudBackupsApi.GetCompliancePolicy(ctx, projectID).Execute() if err != nil { - if apiResp == nil || apiResp.StatusCode != http.StatusNotFound { + if !util.StatusNotFound(apiResp) { return handleError(apiResp, constants.LIST, err) } return handler.ProgressEvent{ @@ -274,7 +274,7 @@ func validateProgress(client *util.MongoDBClient, model *Model, isDelete bool) h projectID := *model.ProjectId policy, resp, err := client.AtlasSDK.CloudBackupsApi.GetCompliancePolicy(ctx, projectID).Execute() - notFound := resp != nil && resp.StatusCode == http.StatusNotFound + notFound := util.StatusNotFound(resp) policyDeleted := notFound || (policy != nil && policy.GetProjectId() == "") if err != nil && !notFound { From 52bc66f202ffa2046ae883e48de225ac2176338f Mon Sep 17 00:00:00 2001 From: ParthasarathyV Date: Wed, 21 Jan 2026 15:49:28 -0500 Subject: [PATCH 3/5] CLOUDP-369800-backup-compliance-policy few enhancements --- .../cmd/resource/handlers.go | 10 ++-- .../cmd/resource/mappings.go | 5 ++ .../backup-compliance-policy/docs/README.md | 48 +++++++++---------- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/cfn-resources/backup-compliance-policy/cmd/resource/handlers.go b/cfn-resources/backup-compliance-policy/cmd/resource/handlers.go index a33555827..5ea9253e3 100644 --- a/cfn-resources/backup-compliance-policy/cmd/resource/handlers.go +++ b/cfn-resources/backup-compliance-policy/cmd/resource/handlers.go @@ -82,7 +82,7 @@ func checkPolicyNotFound(policy *admin.DataProtectionSettings20231001, apiResp * func HandleCreate(req *handler.Request, client *util.MongoDBClient, model *Model) handler.ProgressEvent { if IsCallback(req) { - return validateProgress(client, model, false) + return validateProgress(client, model, false, constants.CREATE) } ctx := context.Background() @@ -145,7 +145,7 @@ func HandleRead(req *handler.Request, client *util.MongoDBClient, model *Model) func HandleUpdate(req *handler.Request, client *util.MongoDBClient, model *Model) handler.ProgressEvent { if IsCallback(req) { - return validateProgress(client, model, false) + return validateProgress(client, model, false, constants.UPDATE) } ctx := context.Background() @@ -179,7 +179,7 @@ func HandleUpdate(req *handler.Request, client *util.MongoDBClient, model *Model func HandleDelete(req *handler.Request, client *util.MongoDBClient, model *Model) handler.ProgressEvent { if IsCallback(req) { - return validateProgress(client, model, true) + return validateProgress(client, model, true, constants.DELETE) } ctx := context.Background() @@ -269,7 +269,7 @@ func inProgressEvent(model *Model, policy *admin.DataProtectionSettings20231001) } } -func validateProgress(client *util.MongoDBClient, model *Model, isDelete bool) handler.ProgressEvent { +func validateProgress(client *util.MongoDBClient, model *Model, isDelete bool, operation constants.CfnFunctions) handler.ProgressEvent { ctx := context.Background() projectID := *model.ProjectId @@ -281,7 +281,7 @@ func validateProgress(client *util.MongoDBClient, model *Model, isDelete bool) h if isPendingActionError(err) { return inProgressEvent(model, policy) } - return handleError(resp, constants.CREATE, err) + return handleError(resp, operation, err) } if isDelete && policyDeleted { diff --git a/cfn-resources/backup-compliance-policy/cmd/resource/mappings.go b/cfn-resources/backup-compliance-policy/cmd/resource/mappings.go index bdd140a65..8a0251ece 100644 --- a/cfn-resources/backup-compliance-policy/cmd/resource/mappings.go +++ b/cfn-resources/backup-compliance-policy/cmd/resource/mappings.go @@ -233,6 +233,11 @@ func ExpandOnDemandPolicyItem(item *OnDemandPolicyItem) *admin.BackupComplianceO } func ExpandScheduledPolicyItem(item *ScheduledPolicyItem, frequencyType string) admin.BackupComplianceScheduledPolicyItem { + if item == nil { + return admin.BackupComplianceScheduledPolicyItem{ + FrequencyType: frequencyType, + } + } frequencyInterval := 0 if item.FrequencyInterval != nil { frequencyInterval = *item.FrequencyInterval diff --git a/cfn-resources/backup-compliance-policy/docs/README.md b/cfn-resources/backup-compliance-policy/docs/README.md index 50b2576e0..9ded45e5b 100644 --- a/cfn-resources/backup-compliance-policy/docs/README.md +++ b/cfn-resources/backup-compliance-policy/docs/README.md @@ -230,51 +230,51 @@ ISO 8601 timestamp in UTC of when the Backup Compliance Policy settings were las Email address of the user who last updated the Backup Compliance Policy settings. -#### Id +#### OnDemandPolicyItem.Id -Returns the Id value. +Unique identifier of the on-demand backup policy item. -#### FrequencyType +#### OnDemandPolicyItem.FrequencyType -Returns the FrequencyType value. +Frequency type of the on-demand backup policy item. Always returns "ondemand". -#### Id +#### PolicyItemHourly.Id -Returns the Id value. +Unique identifier of the hourly backup policy item. -#### FrequencyType +#### PolicyItemHourly.FrequencyType -Returns the FrequencyType value. +Frequency type of the hourly backup policy item. Always returns "hourly". -#### Id +#### PolicyItemDaily.Id -Returns the Id value. +Unique identifier of the daily backup policy item. -#### FrequencyType +#### PolicyItemDaily.FrequencyType -Returns the FrequencyType value. +Frequency type of the daily backup policy item. Always returns "daily". -#### Id +#### PolicyItemWeekly.Id -Returns the Id value. +Unique identifier of the weekly backup policy item(s). For arrays, use index notation (e.g., PolicyItemWeekly[0].Id). -#### FrequencyType +#### PolicyItemWeekly.FrequencyType -Returns the FrequencyType value. +Frequency type of the weekly backup policy item(s). Always returns "weekly". For arrays, use index notation (e.g., PolicyItemWeekly[0].FrequencyType). -#### Id +#### PolicyItemMonthly.Id -Returns the Id value. +Unique identifier of the monthly backup policy item(s). For arrays, use index notation (e.g., PolicyItemMonthly[0].Id). -#### FrequencyType +#### PolicyItemMonthly.FrequencyType -Returns the FrequencyType value. +Frequency type of the monthly backup policy item(s). Always returns "monthly". For arrays, use index notation (e.g., PolicyItemMonthly[0].FrequencyType). -#### Id +#### PolicyItemYearly.Id -Returns the Id value. +Unique identifier of the yearly backup policy item(s). For arrays, use index notation (e.g., PolicyItemYearly[0].Id). -#### FrequencyType +#### PolicyItemYearly.FrequencyType -Returns the FrequencyType value. +Frequency type of the yearly backup policy item(s). Always returns "yearly". For arrays, use index notation (e.g., PolicyItemYearly[0].FrequencyType). From 4f1e9f8bb1ddd2d5568239b8cb87f5c1dc6e48f5 Mon Sep 17 00:00:00 2001 From: Rakhul S Prakash Date: Thu, 22 Jan 2026 10:31:36 +0530 Subject: [PATCH 4/5] Update handler to remove model --- .../cmd/resource/handlers.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cfn-resources/backup-compliance-policy/cmd/resource/handlers.go b/cfn-resources/backup-compliance-policy/cmd/resource/handlers.go index 5ea9253e3..0c8fee9d4 100644 --- a/cfn-resources/backup-compliance-policy/cmd/resource/handlers.go +++ b/cfn-resources/backup-compliance-policy/cmd/resource/handlers.go @@ -166,6 +166,9 @@ func HandleUpdate(req *handler.Request, client *util.MongoDBClient, model *Model _, apiResp, err = client.AtlasSDK.CloudBackupsApi.UpdateCompliancePolicyWithParams(ctx, ¶ms).Execute() if err != nil { + if isPendingActionError(err) { + return handlePendingAction(ctx, client, model, projectID) + } return handleError(apiResp, constants.UPDATE, err) } @@ -233,7 +236,7 @@ func HandleList(req *handler.Request, client *util.MongoDBClient, model *Model) return handler.ProgressEvent{ OperationStatus: handler.Success, Message: constants.Complete, - ResourceModels: []interface{}{}, + ResourceModels: []any{}, } } @@ -241,18 +244,16 @@ func HandleList(req *handler.Request, client *util.MongoDBClient, model *Model) return handler.ProgressEvent{ OperationStatus: handler.Success, Message: constants.Complete, - ResourceModels: []interface{}{}, + ResourceModels: []any{}, } } - listModel := &Model{ProjectId: &projectID} - SetBackupCompliancePolicyData(listModel, policy) - model = listModel + SetBackupCompliancePolicyData(model, policy) return handler.ProgressEvent{ OperationStatus: handler.Success, Message: constants.Complete, - ResourceModels: []interface{}{model}, + ResourceModels: []any{model}, } } From 3d94e0e244a1177bec50901e079d30cea80d6f6e Mon Sep 17 00:00:00 2001 From: Rakhul S Prakash Date: Thu, 29 Jan 2026 11:00:15 +0530 Subject: [PATCH 5/5] Address review comment --- .../cmd/resource/mappings.go | 51 ++++++++----------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/cfn-resources/backup-compliance-policy/cmd/resource/mappings.go b/cfn-resources/backup-compliance-policy/cmd/resource/mappings.go index 8a0251ece..cb0047ae1 100644 --- a/cfn-resources/backup-compliance-policy/cmd/resource/mappings.go +++ b/cfn-resources/backup-compliance-policy/cmd/resource/mappings.go @@ -41,14 +41,15 @@ func SetBackupCompliancePolicyData(currentModel *Model, policy *admin.DataProtec return } - if policy.ProjectId != nil { - currentModel.ProjectId = policy.ProjectId - } + projectID := policy.GetProjectId() + currentModel.ProjectId = &projectID authorizedEmail := policy.GetAuthorizedEmail() currentModel.AuthorizedEmail = &authorizedEmail + authorizedUserFirstName := policy.GetAuthorizedUserFirstName() currentModel.AuthorizedUserFirstName = &authorizedUserFirstName + authorizedUserLastName := policy.GetAuthorizedUserLastName() currentModel.AuthorizedUserLastName = &authorizedUserLastName @@ -211,49 +212,37 @@ func ExpandOnDemandPolicyItem(item *OnDemandPolicyItem) *admin.BackupComplianceO if item == nil { return nil } - frequencyInterval := 0 + onDemandPolicy := &admin.BackupComplianceOnDemandPolicyItem{ + Id: item.Id, + FrequencyType: "ondemand", + } if item.FrequencyInterval != nil { - frequencyInterval = *item.FrequencyInterval + onDemandPolicy.FrequencyInterval = *item.FrequencyInterval } - retentionValue := 0 if item.RetentionValue != nil { - retentionValue = *item.RetentionValue + onDemandPolicy.RetentionValue = *item.RetentionValue } - retentionUnit := "" if item.RetentionUnit != nil { - retentionUnit = *item.RetentionUnit - } - return &admin.BackupComplianceOnDemandPolicyItem{ - Id: item.Id, - FrequencyInterval: frequencyInterval, - FrequencyType: "ondemand", - RetentionUnit: retentionUnit, - RetentionValue: retentionValue, + onDemandPolicy.RetentionUnit = *item.RetentionUnit } + return onDemandPolicy } func ExpandScheduledPolicyItem(item *ScheduledPolicyItem, frequencyType string) admin.BackupComplianceScheduledPolicyItem { + scheduledPolicy := admin.BackupComplianceScheduledPolicyItem{ + FrequencyType: frequencyType, + } if item == nil { - return admin.BackupComplianceScheduledPolicyItem{ - FrequencyType: frequencyType, - } + return scheduledPolicy } - frequencyInterval := 0 if item.FrequencyInterval != nil { - frequencyInterval = *item.FrequencyInterval + scheduledPolicy.FrequencyInterval = *item.FrequencyInterval } - retentionValue := 0 if item.RetentionValue != nil { - retentionValue = *item.RetentionValue + scheduledPolicy.RetentionValue = *item.RetentionValue } - retentionUnit := "" if item.RetentionUnit != nil { - retentionUnit = *item.RetentionUnit - } - return admin.BackupComplianceScheduledPolicyItem{ - FrequencyType: frequencyType, - FrequencyInterval: frequencyInterval, - RetentionUnit: retentionUnit, - RetentionValue: retentionValue, + scheduledPolicy.RetentionUnit = *item.RetentionUnit } + return scheduledPolicy }