From 920d4aaf7b0d24b61e97b4468dc8b70ca6c19024 Mon Sep 17 00:00:00 2001 From: Rakhul S Prakash Date: Thu, 29 Jan 2026 10:32:29 +0530 Subject: [PATCH 1/3] feat: add log-integration resource --- .github/workflows/contract-testing.yaml | 43 +++++ cfn-resources/log-integration/.rpdk-config | 12 ++ cfn-resources/log-integration/Makefile | 37 ++++ cfn-resources/log-integration/README.md | 20 ++ cfn-resources/log-integration/cmd/main.go | 85 +++++++++ .../log-integration/cmd/resource/config.go | 19 ++ .../log-integration/cmd/resource/handlers.go | 111 +++++++++++ .../log-integration/cmd/resource/mappings.go | 66 +++++++ .../cmd/resource/mappings_test.go | 91 +++++++++ .../log-integration/cmd/resource/model.go | 16 ++ .../log-integration/cmd/resource/resource.go | 87 +++++++++ cfn-resources/log-integration/docs/README.md | 136 ++++++++++++++ .../mongodb-atlas-logintegration.json | 83 +++++++++ .../log-integration/resource-role.yaml | 38 ++++ cfn-resources/log-integration/template.yml | 26 +++ .../test/cfn-test-create-inputs.sh | 57 ++++++ .../test/cfn-test-delete-inputs.sh | 12 ++ .../test/contract-testing/cfn-test-create.sh | 20 ++ .../test/contract-testing/cfn-test-delete.sh | 15 ++ .../test/inputs_1_create.template.json | 11 ++ .../test/inputs_1_update.template.json | 13 ++ .../log-integration.sample-cfn-request.json | 25 +++ cfn-resources/util/constants/constants.go | 6 + examples/log-integration/README.md | 30 +++ examples/log-integration/log-integration.json | 174 ++++++++++++++++++ 25 files changed, 1233 insertions(+) create mode 100644 cfn-resources/log-integration/.rpdk-config create mode 100644 cfn-resources/log-integration/Makefile create mode 100644 cfn-resources/log-integration/README.md create mode 100644 cfn-resources/log-integration/cmd/main.go create mode 100644 cfn-resources/log-integration/cmd/resource/config.go create mode 100644 cfn-resources/log-integration/cmd/resource/handlers.go create mode 100644 cfn-resources/log-integration/cmd/resource/mappings.go create mode 100644 cfn-resources/log-integration/cmd/resource/mappings_test.go create mode 100644 cfn-resources/log-integration/cmd/resource/model.go create mode 100644 cfn-resources/log-integration/cmd/resource/resource.go create mode 100644 cfn-resources/log-integration/docs/README.md create mode 100644 cfn-resources/log-integration/mongodb-atlas-logintegration.json create mode 100644 cfn-resources/log-integration/resource-role.yaml create mode 100644 cfn-resources/log-integration/template.yml create mode 100755 cfn-resources/log-integration/test/cfn-test-create-inputs.sh create mode 100755 cfn-resources/log-integration/test/cfn-test-delete-inputs.sh create mode 100755 cfn-resources/log-integration/test/contract-testing/cfn-test-create.sh create mode 100755 cfn-resources/log-integration/test/contract-testing/cfn-test-delete.sh create mode 100644 cfn-resources/log-integration/test/inputs_1_create.template.json create mode 100644 cfn-resources/log-integration/test/inputs_1_update.template.json create mode 100644 cfn-resources/log-integration/test/log-integration.sample-cfn-request.json create mode 100644 examples/log-integration/README.md create mode 100644 examples/log-integration/log-integration.json diff --git a/.github/workflows/contract-testing.yaml b/.github/workflows/contract-testing.yaml index 3173da05d..6440f923b 100644 --- a/.github/workflows/contract-testing.yaml +++ b/.github/workflows/contract-testing.yaml @@ -23,6 +23,7 @@ jobs: federated-query-limit: ${{ steps.filter.outputs.federated-query-limit }} federated-settings-identity-provider: ${{ steps.filter.outputs.federated-settings-identity-provider }} flex-cluster: ${{ steps.filter.outputs.flex-cluster }} + log-integration: ${{ steps.filter.outputs.log-integration }} online-archive: ${{ steps.filter.outputs.online-archive }} organization: ${{ steps.filter.outputs.organization }} service-account: ${{ steps.filter.outputs.service-account }} @@ -68,6 +69,8 @@ jobs: - 'cfn-resources/federated-settings-identity-provider/**' flex-cluster: - 'cfn-resources/flex-cluster/**' + log-integration: + - 'cfn-resources/log-integration/**' online-archive: - 'cfn-resources/online-archive/**' organization: @@ -588,6 +591,46 @@ jobs: make run-contract-testing make delete-test-resources + log-integration: + needs: change-detection + if: ${{ needs.change-detection.outputs.log-integration == '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: | + pushd cfn-resources/log-integration + make create-test-resources + + cat inputs/* + + make run-contract-testing + make delete-test-resources online-archive: needs: change-detection if: ${{ needs.change-detection.outputs.online-archive == 'true' }} diff --git a/cfn-resources/log-integration/.rpdk-config b/cfn-resources/log-integration/.rpdk-config new file mode 100644 index 000000000..26392b300 --- /dev/null +++ b/cfn-resources/log-integration/.rpdk-config @@ -0,0 +1,12 @@ +{ + "typeName": "MongoDB::Atlas::LogIntegration", + "language": "go", + "runtime": "provided.al2", + "entrypoint": "bootstrap", + "testEntrypoint": "bootstrap", + "settings": { + "import_path": "github.com/mongodb/mongodbatlas-cloudformation-resources/log-integration", + "protocolVersion": "2.0.0", + "pluginVersion": "2.0.4" + } +} diff --git a/cfn-resources/log-integration/Makefile b/cfn-resources/log-integration/Makefile new file mode 100644 index 000000000..4f3d93ebd --- /dev/null +++ b/cfn-resources/log-integration/Makefile @@ -0,0 +1,37 @@ +.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/debug cmd/main.go + +clean: + rm -rf bin + +submit: clean build # submit to private registry must use release build not debug build + @echo "==> Submitting to private registry for testing" + cfn submit --set-default --region us-east-1 + +create-test-resources: + @echo "==> Creating test files and resources for contract testing" + ./test/contract-testing/cfn-test-create.sh + +delete-test-resources: + @echo "==> Delete test resources used for contract testing" + ./test/contract-testing/cfn-test-delete.sh + +run-contract-testing: + @echo "==> Run contract testing" + make build + sam local start-lambda & + cfn test --function-name TestEntrypoint --verbose diff --git a/cfn-resources/log-integration/README.md b/cfn-resources/log-integration/README.md new file mode 100644 index 000000000..e12133bae --- /dev/null +++ b/cfn-resources/log-integration/README.md @@ -0,0 +1,20 @@ +# MongoDB::Atlas::LogIntegration + +## Description + +The log integration resource provides access to push-based log export configurations for MongoDB Atlas. The resource allows you to create, edit and delete log export integrations to AWS S3 buckets. Push-based log export enables you to automatically export MongoDB Atlas logs to your AWS S3 bucket with 1-minute frequency. + +For more information, see [Push Logs to AWS S3 bucket](https://www.mongodb.com/docs/atlas/push-logs/) and the [Push-Based Log Export API](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/group/endpoint-push-based-log-export). + +## 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 example [CFN Template](/examples/log-integration/README.md) for example resource. diff --git a/cfn-resources/log-integration/cmd/main.go b/cfn-resources/log-integration/cmd/main.go new file mode 100644 index 000000000..d66bc29eb --- /dev/null +++ b/cfn-resources/log-integration/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/log-integration/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/log-integration/cmd/resource/config.go b/cfn-resources/log-integration/cmd/resource/config.go new file mode 100644 index 000000000..4d9eb7831 --- /dev/null +++ b/cfn-resources/log-integration/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/log-integration/cmd/resource/handlers.go b/cfn-resources/log-integration/cmd/resource/handlers.go new file mode 100644 index 000000000..0b47c0248 --- /dev/null +++ b/cfn-resources/log-integration/cmd/resource/handlers.go @@ -0,0 +1,111 @@ +// 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" + + "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/progressevent" +) + +func HandleCreate(req *handler.Request, client *util.MongoDBClient, model *Model) handler.ProgressEvent { + s3LogIntegrationReq := NewLogIntegrationCreateRequest(model) + logIntegrationResp, resp, err := client.AtlasSDK.PushBasedLogExportApi.CreateGroupLogIntegration(context.Background(), *model.ProjectId, s3LogIntegrationReq).Execute() + if err != nil { + return handleError(resp, err, "Error creating log integration") + } + + UpdateLogIntegrationModel(model, logIntegrationResp) + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: "Create complete", + ResourceModel: model, + } +} + +func HandleRead(req *handler.Request, client *util.MongoDBClient, model *Model) handler.ProgressEvent { + logIntegrationResp, resp, err := client.AtlasSDK.PushBasedLogExportApi.GetGroupLogIntegration(context.Background(), *model.ProjectId, *model.IntegrationId).Execute() + if err != nil { + return handleError(resp, err, "Error reading log integration") + } + + UpdateLogIntegrationModel(model, logIntegrationResp) + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.ReadComplete, + ResourceModel: model, + } +} + +func HandleUpdate(req *handler.Request, client *util.MongoDBClient, model *Model) handler.ProgressEvent { + logIntegrationReq := NewLogIntegrationUpdateRequest(model) + logIntegrationResp, resp, err := client.AtlasSDK.PushBasedLogExportApi.UpdateGroupLogIntegration(context.Background(), *model.ProjectId, *model.IntegrationId, logIntegrationReq).Execute() + if err != nil { + return handleError(resp, err, "Error updating log integration") + } + + UpdateLogIntegrationModel(model, logIntegrationResp) + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: "Update complete", + ResourceModel: model, + } +} + +func HandleDelete(req *handler.Request, client *util.MongoDBClient, model *Model) handler.ProgressEvent { + resp, err := client.AtlasSDK.PushBasedLogExportApi.DeleteGroupLogIntegration(context.Background(), *model.ProjectId, *model.IntegrationId).Execute() + if err != nil { + return handleError(resp, err, "Error deleting log integration") + } + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: "Delete complete", + } +} + +func HandleList(req *handler.Request, client *util.MongoDBClient, model *Model) handler.ProgressEvent { + paginatedResp, resp, err := client.AtlasSDK.PushBasedLogExportApi.ListGroupLogIntegrations(context.Background(), *model.ProjectId).Execute() + if err != nil { + return handleError(resp, err, "Error listing log integrations") + } + + var allModels []*Model + results := paginatedResp.GetResults() + for i := range results { + modelItem := &Model{ + ProjectId: model.ProjectId, + Profile: model.Profile, + } + UpdateLogIntegrationModel(modelItem, &results[i]) + allModels = append(allModels, modelItem) + } + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.Complete, + ResourceModel: allModels, + } +} + +func handleError(resp *http.Response, err error, message string) handler.ProgressEvent { + errMsg := fmt.Sprintf("%s: %v", message, err) + return progressevent.GetFailedEventByResponse(errMsg, resp) +} diff --git a/cfn-resources/log-integration/cmd/resource/mappings.go b/cfn-resources/log-integration/cmd/resource/mappings.go new file mode 100644 index 000000000..768263e06 --- /dev/null +++ b/cfn-resources/log-integration/cmd/resource/mappings.go @@ -0,0 +1,66 @@ +// 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/v20250312013/admin" + +func NewLogIntegrationCreateRequest(model *Model) *admin.S3LogIntegrationRequest { + if model == nil { + return nil + } + req := &admin.S3LogIntegrationRequest{ + Type: *model.Type, + BucketName: *model.BucketName, + IamRoleId: *model.IamRoleId, + PrefixPath: *model.PrefixPath, + LogTypes: model.LogTypes, + } + if model.KmsKey != nil && *model.KmsKey != "" { + req.KmsKey = model.KmsKey + } + return req +} + +func NewLogIntegrationUpdateRequest(model *Model) *admin.LogIntegrationRequest { + if model == nil { + return nil + } + req := &admin.LogIntegrationRequest{ + Type: *model.Type, + BucketName: model.BucketName, + IamRoleId: model.IamRoleId, + PrefixPath: model.PrefixPath, + LogTypes: &model.LogTypes, + } + if model.KmsKey != nil && *model.KmsKey != "" { + req.KmsKey = model.KmsKey + } + return req +} + +func UpdateLogIntegrationModel(model *Model, logIntegrationResp *admin.LogIntegrationResponse) { + if logIntegrationResp == nil { + return + } + model.IntegrationId = &logIntegrationResp.Id + model.BucketName = logIntegrationResp.BucketName + model.IamRoleId = logIntegrationResp.IamRoleId + model.PrefixPath = logIntegrationResp.PrefixPath + model.Type = &logIntegrationResp.Type + model.KmsKey = logIntegrationResp.KmsKey + if logIntegrationResp.LogTypes != nil { + model.LogTypes = *logIntegrationResp.LogTypes + } +} diff --git a/cfn-resources/log-integration/cmd/resource/mappings_test.go b/cfn-resources/log-integration/cmd/resource/mappings_test.go new file mode 100644 index 000000000..bdd96059d --- /dev/null +++ b/cfn-resources/log-integration/cmd/resource/mappings_test.go @@ -0,0 +1,91 @@ +// 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" + + "github.com/mongodb/mongodbatlas-cloudformation-resources/log-integration/cmd/resource" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/atlas-sdk/v20250312013/admin" +) + +func TestNewLogIntegrationCreateRequest(t *testing.T) { + model := &resource.Model{ + Type: util.StringPtr("S3"), + BucketName: util.StringPtr("test-bucket"), + IamRoleId: util.StringPtr("arn:aws:iam::123456789012:role/test-role"), + PrefixPath: util.StringPtr("/logs"), + LogTypes: []string{"AUDIT", "FTDC"}, + KmsKey: util.StringPtr("arn:aws:kms:us-east-1:123456789012:key/test-key"), + } + + result := resource.NewLogIntegrationCreateRequest(model) + + require.NotNil(t, result) + assert.Equal(t, util.SafeString(model.Type), result.Type) + assert.Equal(t, util.SafeString(model.BucketName), result.BucketName) + assert.Equal(t, util.SafeString(model.IamRoleId), result.IamRoleId) + assert.Equal(t, util.SafeString(model.PrefixPath), result.PrefixPath) + assert.Equal(t, model.LogTypes, result.LogTypes) + assert.Equal(t, util.StringPtr("arn:aws:kms:us-east-1:123456789012:key/test-key"), result.KmsKey) +} + +func TestNewLogIntegrationUpdateRequest(t *testing.T) { + model := &resource.Model{ + Type: util.StringPtr("S3"), + BucketName: util.StringPtr("test-bucket"), + IamRoleId: util.StringPtr("arn:aws:iam::123456789012:role/test-role"), + PrefixPath: util.StringPtr("/logs"), + LogTypes: []string{"AUDIT", "PROFILER"}, + KmsKey: util.StringPtr("arn:aws:kms:us-east-1:123456789012:key/test-key"), + } + + result := resource.NewLogIntegrationUpdateRequest(model) + + require.NotNil(t, result) + assert.Equal(t, util.SafeString(model.Type), result.Type) + assert.Equal(t, model.BucketName, result.BucketName) + assert.Equal(t, model.IamRoleId, result.IamRoleId) + assert.Equal(t, model.PrefixPath, result.PrefixPath) + require.NotNil(t, result.LogTypes) + assert.Equal(t, model.LogTypes, *result.LogTypes) + assert.Equal(t, util.StringPtr("arn:aws:kms:us-east-1:123456789012:key/test-key"), result.KmsKey) +} + +func TestUpdateLogIntegrationModel(t *testing.T) { + model := &resource.Model{} + response := &admin.LogIntegrationResponse{ + Id: "integration-123", + Type: "S3", + BucketName: admin.PtrString("test-bucket"), + IamRoleId: admin.PtrString("arn:aws:iam::123456789012:role/test-role"), + PrefixPath: admin.PtrString("/logs"), + LogTypes: &[]string{"AUDIT", "FTDC"}, + KmsKey: admin.PtrString("arn:aws:kms:us-east-1:123456789012:key/test-key"), + } + + resource.UpdateLogIntegrationModel(model, response) + + assert.Equal(t, &response.Id, model.IntegrationId) + assert.Equal(t, &response.Type, model.Type) + assert.Equal(t, response.BucketName, model.BucketName) + assert.Equal(t, response.IamRoleId, model.IamRoleId) + assert.Equal(t, response.PrefixPath, model.PrefixPath) + assert.Equal(t, response.KmsKey, model.KmsKey) + assert.Equal(t, *response.LogTypes, model.LogTypes) +} diff --git a/cfn-resources/log-integration/cmd/resource/model.go b/cfn-resources/log-integration/cmd/resource/model.go new file mode 100644 index 000000000..83a7d2dd2 --- /dev/null +++ b/cfn-resources/log-integration/cmd/resource/model.go @@ -0,0 +1,16 @@ +// 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"` + IntegrationId *string `json:",omitempty"` + Type *string `json:",omitempty"` + BucketName *string `json:",omitempty"` + IamRoleId *string `json:",omitempty"` + PrefixPath *string `json:",omitempty"` + KmsKey *string `json:",omitempty"` + LogTypes []string `json:",omitempty"` +} diff --git a/cfn-resources/log-integration/cmd/resource/resource.go b/cfn-resources/log-integration/cmd/resource/resource.go new file mode 100644 index 000000000..c84406331 --- /dev/null +++ b/cfn-resources/log-integration/cmd/resource/resource.go @@ -0,0 +1,87 @@ +// 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/mongodb/mongodbatlas-cloudformation-resources/util" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/constants" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/validator" + + "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" +) + +var ( + createRequiredFields = []string{constants.ProjectID, constants.Type, constants.BucketName, constants.IamRoleID, constants.PrefixPath, constants.LogTypes} + readUpdateDeleteRequiredFields = []string{constants.ProjectID, constants.IntegrationID} + listRequiredFields = []string{constants.ProjectID} +) + +// Create handles the Create event from the Cloudformation service. +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 +} + +// Read handles the Read event from the Cloudformation service. +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 +} + +// Update handles the Update event from the Cloudformation service. +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 +} + +// Delete handles the Delete event from the Cloudformation service. +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 +} + +// List handles the List event from the Cloudformation service. +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-logintegration") + if modelValidation := validator.ValidateModel(requiredFields, model); modelValidation != nil { + return nil, modelValidation + } + util.SetDefaultProfileIfNotDefined(&model.Profile) + client, peErr := util.NewAtlasClient(&req, model.Profile) + if peErr != nil { + return nil, peErr + } + return client, nil +} diff --git a/cfn-resources/log-integration/docs/README.md b/cfn-resources/log-integration/docs/README.md new file mode 100644 index 000000000..402ff43e5 --- /dev/null +++ b/cfn-resources/log-integration/docs/README.md @@ -0,0 +1,136 @@ +# MongoDB::Atlas::LogIntegration + +The log integration resource provides access to push-based log export configurations for MongoDB Atlas. The resource allows you to create, edit and delete log export integrations to AWS S3 buckets. The resource requires your Project ID. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "MongoDB::Atlas::LogIntegration",
+    "Properties" : {
+        "Profile" : String,
+        "ProjectId" : String,
+        "Type" : String,
+        "BucketName" : String,
+        "IamRoleId" : String,
+        "PrefixPath" : String,
+        "KmsKey" : String,
+        "LogTypes" : [ String, ... ]
+    }
+}
+
+ +### YAML + +
+Type: MongoDB::Atlas::LogIntegration
+Properties:
+    Profile: String
+    ProjectId: String
+    Type: String
+    BucketName: String
+    IamRoleId: String
+    PrefixPath: String
+    KmsKey: String
+    LogTypes: 
+      - String
+
+ +## 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_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ProjectId + +Unique 24-hexadecimal digit string that identifies your project. Use the /groups endpoint to retrieve all projects to which the authenticated user has access. Groups and projects are synonymous terms. Your group id is the same as your project id. + +_Required_: Yes + +_Type_: String + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### Type + +Human-readable label that identifies the service to which you want to integrate with MongoDB Cloud. The value must match the log integration type. + +_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) + +#### BucketName + +Human-readable label that identifies the S3 bucket name for storing log files. + +_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) + +#### IamRoleId + +Unique 24-hexadecimal digit string that identifies the AWS IAM role that MongoDB Cloud uses to access your S3 bucket. + +_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) + +#### PrefixPath + +S3 directory path prefix where the log files will be stored. MongoDB Cloud will add further sub-directories based on the log type. + +_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) + +#### KmsKey + +AWS KMS key ID or ARN for server-side encryption (optional). If not provided, uses bucket default encryption settings. + +_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) + +#### LogTypes + +Array of log types to export to S3. Valid values: MONGOD, MONGOS, MONGOD_AUDIT, MONGOS_AUDIT. + +_Required_: Yes + +_Type_: List of String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +## Return Values + +### Fn::GetAtt + +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). + +#### IntegrationId + +Unique 24-character hexadecimal digit string that identifies the log integration configuration. + diff --git a/cfn-resources/log-integration/mongodb-atlas-logintegration.json b/cfn-resources/log-integration/mongodb-atlas-logintegration.json new file mode 100644 index 000000000..4868f3a3f --- /dev/null +++ b/cfn-resources/log-integration/mongodb-atlas-logintegration.json @@ -0,0 +1,83 @@ +{ + "typeName": "MongoDB::Atlas::LogIntegration", + "description": "The log integration resource provides access to push-based log export configurations for MongoDB Atlas. The resource allows you to create, edit and delete log export integrations to AWS S3 buckets. The resource requires your Project ID.", + "definitions": {}, + "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": "Unique 24-hexadecimal digit string that identifies your project. Use the /groups endpoint to retrieve all projects to which the authenticated user has access. Groups and projects are synonymous terms. Your group id is the same as your project id.", + "type": "string" + }, + "IntegrationId": { + "description": "Unique 24-character hexadecimal digit string that identifies the log integration configuration.", + "type": "string" + }, + "Type": { + "description": "Human-readable label that identifies the service to which you want to integrate with MongoDB Cloud. The value must match the log integration type.", + "type": "string" + }, + "BucketName": { + "description": "Human-readable label that identifies the S3 bucket name for storing log files.", + "type": "string" + }, + "IamRoleId": { + "description": "Unique 24-hexadecimal digit string that identifies the AWS IAM role that MongoDB Cloud uses to access your S3 bucket.", + "type": "string" + }, + "PrefixPath": { + "description": "S3 directory path prefix where the log files will be stored. MongoDB Cloud will add further sub-directories based on the log type.", + "type": "string" + }, + "KmsKey": { + "description": "AWS KMS key ID or ARN for server-side encryption (optional). If not provided, uses bucket default encryption settings.", + "type": "string" + }, + "LogTypes": { + "description": "Array of log types to export to S3. Valid values: MONGOD, MONGOS, MONGOD_AUDIT, MONGOS_AUDIT.", + "type": "array", + "insertionOrder": false, + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "ProjectId", + "Type", + "BucketName", + "IamRoleId", + "PrefixPath", + "LogTypes" + ], + "readOnlyProperties": ["/properties/IntegrationId"], + "createOnlyProperties": ["/properties/ProjectId", "/properties/Profile"], + "primaryIdentifier": [ + "/properties/ProjectId", + "/properties/IntegrationId", + "/properties/Profile" + ], + "handlers": { + "create": { + "permissions": ["secretsmanager:GetSecretValue"] + }, + "read": { + "permissions": ["secretsmanager:GetSecretValue"] + }, + "update": { + "permissions": ["secretsmanager:GetSecretValue"] + }, + "delete": { + "permissions": ["secretsmanager:GetSecretValue"] + } + }, + "documentationUrl": "https://github.com/mongodb/mongodbatlas-cloudformation-resources/blob/master/cfn-resources/log-integration/README.md", + "tagging": { + "taggable": false + }, + "sourceUrl": "https://github.com/mongodb/mongodbatlas-cloudformation-resources/tree/master/cfn-resources/log-integration" +} diff --git a/cfn-resources/log-integration/resource-role.yaml b/cfn-resources/log-integration/resource-role.yaml new file mode 100644 index 000000000..e40e7c206 --- /dev/null +++ b/cfn-resources/log-integration/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-LogIntegration/* + 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/log-integration/template.yml b/cfn-resources/log-integration/template.yml new file mode 100644 index 000000000..251830706 --- /dev/null +++ b/cfn-resources/log-integration/template.yml @@ -0,0 +1,26 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the MongoDB::Atlas::LogIntegration resource type + +Globals: + Function: + Timeout: 60 + +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/log-integration/test/cfn-test-create-inputs.sh b/cfn-resources/log-integration/test/cfn-test-create-inputs.sh new file mode 100755 index 000000000..37f0d300a --- /dev/null +++ b/cfn-resources/log-integration/test/cfn-test-create-inputs.sh @@ -0,0 +1,57 @@ +#!/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 "usage:$0 " + echo "Generates test input files for log integration" + exit 0 +} + +if [ "$#" -ne 3 ]; then usage; fi +if [[ "$*" == help ]]; then usage; fi + +rm -rf inputs +mkdir inputs + +#set profile - relevant for contract tests which define a custom profile +profile="default" +if [ ${MONGODB_ATLAS_PROFILE+x} ]; then + echo "profile set to ${MONGODB_ATLAS_PROFILE}" + profile=${MONGODB_ATLAS_PROFILE} +fi + +projectName="${1}" +bucketName="${2}" +iamRoleId="${3}" + +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 + +echo "bucketName: $bucketName" +echo "iamRoleId: $iamRoleId" + +WORDTOREMOVE="template." +cd "$(dirname "$0")" || exit +for inputFile in inputs_*; do + outputFile=${inputFile//$WORDTOREMOVE/} + jq --arg projectId "$projectId" \ + --arg bucketName "$bucketName" \ + --arg iamRoleId "$iamRoleId" \ + --arg profile "$profile" \ + '.Profile?|=$profile | .ProjectId?|=$projectId | .BucketName?|=$bucketName | .IamRoleId?|=$iamRoleId' \ + "$inputFile" >"../inputs/$outputFile" +done +cd .. +ls -l inputs diff --git a/cfn-resources/log-integration/test/cfn-test-delete-inputs.sh b/cfn-resources/log-integration/test/cfn-test-delete-inputs.sh new file mode 100755 index 000000000..3dffa7002 --- /dev/null +++ b/cfn-resources/log-integration/test/cfn-test-delete-inputs.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# cfn-test-delete-inputs.sh +# +# This tool deletes test input files and cleans up resources. +# + +set -o errexit +set -o nounset +set -o pipefail + +rm -rf inputs +echo "Deleted inputs directory" diff --git a/cfn-resources/log-integration/test/contract-testing/cfn-test-create.sh b/cfn-resources/log-integration/test/contract-testing/cfn-test-create.sh new file mode 100755 index 000000000..515eec0fa --- /dev/null +++ b/cfn-resources/log-integration/test/contract-testing/cfn-test-create.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# This tool generates the resources and json files in the inputs/ for `cfn test`. +set -o errexit +set -o nounset +set -o pipefail + +projectName="cfn-test-bot-$(date +%s)-$RANDOM" +bucketName="atlas-logs-cfn-test-$RANDOM" +iamRoleId="65a1b2c3d4e5f6a7b8c9d0e1" + +# create project +projectId=$(atlas projects create "${projectName}" --output=json | jq -r '.id') + +echo "projectId: $projectId" +echo "projectName: $projectName" +echo "bucketName: $bucketName" +echo "iamRoleId: $iamRoleId (dummy 24-char hex format)" + +./test/cfn-test-create-inputs.sh "$projectName" "$bucketName" "$iamRoleId" diff --git a/cfn-resources/log-integration/test/contract-testing/cfn-test-delete.sh b/cfn-resources/log-integration/test/contract-testing/cfn-test-delete.sh new file mode 100755 index 000000000..71286ddfb --- /dev/null +++ b/cfn-resources/log-integration/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/log-integration/test/inputs_1_create.template.json b/cfn-resources/log-integration/test/inputs_1_create.template.json new file mode 100644 index 000000000..9eaa5a458 --- /dev/null +++ b/cfn-resources/log-integration/test/inputs_1_create.template.json @@ -0,0 +1,11 @@ +{ + "ProjectId": "", + "Profile": "default", + "Type": "S3_LOG_EXPORT", + "BucketName": "my-s3-bucket-name", + "IamRoleId": "65a1b2c3d4e5f6a7b8c9d0e1", + "PrefixPath": "mongodb-logs", + "LogTypes": [ + "MONGOD_AUDIT" + ] +} diff --git a/cfn-resources/log-integration/test/inputs_1_update.template.json b/cfn-resources/log-integration/test/inputs_1_update.template.json new file mode 100644 index 000000000..86f989c81 --- /dev/null +++ b/cfn-resources/log-integration/test/inputs_1_update.template.json @@ -0,0 +1,13 @@ +{ + "ProjectId": "", + "Profile": "default", + "Type": "S3_LOG_EXPORT", + "BucketName": "my-s3-bucket-name", + "IamRoleId": "65a1b2c3d4e5f6a7b8c9d0e1", + "PrefixPath": "mongodb-logs/updated-path", + "LogTypes": [ + "MONGOD_AUDIT", + "MONGOS_AUDIT", + "MONGOD" + ] +} diff --git a/cfn-resources/log-integration/test/log-integration.sample-cfn-request.json b/cfn-resources/log-integration/test/log-integration.sample-cfn-request.json new file mode 100644 index 000000000..59819aba7 --- /dev/null +++ b/cfn-resources/log-integration/test/log-integration.sample-cfn-request.json @@ -0,0 +1,25 @@ +{ + "credentials": { + "accessKeyId": "", + "secretAccessKey": "", + "sessionToken": "" + }, + "action": "CREATE", + "request": { + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe", + "desiredResourceState": { + "ProjectId": "PROJECT_ID_HERE", + "Profile": "default", + "Type": "S3_LOG_EXPORT", + "BucketName": "my-atlas-logs-bucket", + "IamRoleId": "65a1b2c3d4e5f6a7b8c9d0e1", + "PrefixPath": "mongodb-logs", + "LogTypes": [ + "MONGOD_AUDIT" + ] + }, + "previousResourceState": null, + "logicalResourceIdentifier": null + }, + "callbackContext": null +} diff --git a/cfn-resources/util/constants/constants.go b/cfn-resources/util/constants/constants.go index 6cbfc8a8c..cd7d2900b 100644 --- a/cfn-resources/util/constants/constants.go +++ b/cfn-resources/util/constants/constants.go @@ -168,4 +168,10 @@ const ( AuthorizedEmail = "AuthorizedEmail" AuthorizedUserFirstName = "AuthorizedUserFirstName" AuthorizedUserLastName = "AuthorizedUserLastName" + + IntegrationID = "IntegrationId" + BucketName = "BucketName" + IamRoleID = "IamRoleId" + PrefixPath = "PrefixPath" + LogTypes = "LogTypes" ) diff --git a/examples/log-integration/README.md b/examples/log-integration/README.md new file mode 100644 index 000000000..e8c81dbd6 --- /dev/null +++ b/examples/log-integration/README.md @@ -0,0 +1,30 @@ +# How to create a MongoDB::Atlas::LogIntegration + +## Step 1: Activate the LogIntegration 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::LogIntegration 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 LogIntegration Resource is ready to use. + +## Step 2: Create template using [log-integration.json](log-integration.json) + +Note: Make sure you are providing appropriate values for: + +1. **ProjectId** (required) - Your MongoDB Atlas project ID +2. **BucketName** (required) - S3 bucket name for log export +3. **IamRoleId** (required) - 24-character hex string from Atlas Cloud Provider Access +4. **PrefixPath** (optional) - S3 prefix path (default: "mongodb/logs") +5. **LogType1** (required) - First log type to export (MONGOD, MONGOS, MONGOD_AUDIT, MONGOS_AUDIT) +6. **LogType2** (optional) - Second log type to export +7. **KmsKey** (optional) - AWS KMS key ARN for encryption +8. **Profile** (optional) - Secret Manager profile name (default: "default") diff --git a/examples/log-integration/log-integration.json b/examples/log-integration/log-integration.json new file mode 100644 index 000000000..22fae1a9e --- /dev/null +++ b/examples/log-integration/log-integration.json @@ -0,0 +1,174 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "This template creates a log integration for MongoDB Atlas, enabling export of database logs to AWS S3. This will be billed to your Atlas account.", + "Parameters": { + "ProjectId": { + "Type": "String", + "Description": "Unique 24-hexadecimal digit string that identifies your MongoDB Atlas project" + }, + "Profile": { + "Type": "String", + "Description": "Secret Manager Profile that contains the Atlas Programmatic keys", + "Default": "default" + }, + "BucketName": { + "Type": "String", + "Description": "Name of the AWS S3 bucket to which logs are exported" + }, + "IamRoleId": { + "Type": "String", + "Description": "Unique 24-hexadecimal digit string that identifies the IAM role from MongoDB Atlas Cloud Provider Access. This role must have permissions to access the S3 bucket." + }, + "PrefixPath": { + "Type": "String", + "Description": "S3 bucket prefix path to which logs are exported", + "Default": "mongodb/logs" + }, + "LogType1": { + "Type": "String", + "Description": "First log type to export", + "Default": "MONGOD_AUDIT", + "AllowedValues": [ + "MONGOD", + "MONGOS", + "MONGOD_AUDIT", + "MONGOS_AUDIT" + ] + }, + "LogType2": { + "Type": "String", + "Description": "Second log type to export (optional)", + "Default": "", + "AllowedValues": [ + "", + "MONGOD", + "MONGOS", + "MONGOD_AUDIT", + "MONGOS_AUDIT" + ] + }, + "KmsKey": { + "Type": "String", + "Description": "AWS KMS customer master key ARN for encrypting logs (optional)", + "Default": "" + } + }, + "Conditions": { + "HasLogType2": { + "Fn::Not": [ + { + "Fn::Equals": [ + { + "Ref": "LogType2" + }, + "" + ] + } + ] + }, + "HasKmsKey": { + "Fn::Not": [ + { + "Fn::Equals": [ + { + "Ref": "KmsKey" + }, + "" + ] + } + ] + } + }, + "Resources": { + "AtlasLogIntegration": { + "Type": "MongoDB::Atlas::LogIntegration", + "Properties": { + "ProjectId": { + "Ref": "ProjectId" + }, + "Profile": { + "Ref": "Profile" + }, + "Type": "S3_LOG_EXPORT", + "BucketName": { + "Ref": "BucketName" + }, + "IamRoleId": { + "Ref": "IamRoleId" + }, + "PrefixPath": { + "Ref": "PrefixPath" + }, + "LogTypes": { + "Fn::If": [ + "HasLogType2", + [ + { + "Ref": "LogType1" + }, + { + "Ref": "LogType2" + } + ], + [ + { + "Ref": "LogType1" + } + ] + ] + }, + "KmsKey": { + "Fn::If": [ + "HasKmsKey", + { + "Ref": "KmsKey" + }, + { + "Ref": "AWS::NoValue" + } + ] + } + } + } + }, + "Outputs": { + "IntegrationId": { + "Description": "Unique identifier of the log integration", + "Export": { + "Name": { + "Fn::Sub": "${AWS::StackName}-IntegrationId" + } + }, + "Value": { + "Fn::GetAtt": [ + "AtlasLogIntegration", + "IntegrationId" + ] + } + }, + "ProjectId": { + "Description": "MongoDB Atlas project ID", + "Value": { + "Ref": "ProjectId" + } + }, + "BucketName": { + "Description": "S3 bucket name where logs are exported", + "Value": { + "Ref": "BucketName" + } + }, + "IamRoleId": { + "Description": "IAM role ID used for S3 access", + "Value": { + "Ref": "IamRoleId" + } + }, + "PrefixPath": { + "Description": "S3 bucket prefix path for exported logs", + "Value": { + "Ref": "PrefixPath" + } + } + } +} From 1df1eb84a9910697b9b70981f16a67431d615db5 Mon Sep 17 00:00:00 2001 From: Rakhul S Prakash Date: Thu, 5 Feb 2026 14:55:16 +0530 Subject: [PATCH 2/3] Add valid iamroleId for cfn tests --- .gitignore | 5 ++ .../test/cfn-test-create-inputs.sh | 90 ++++++++++++++++--- .../test/cfn-test-delete-inputs.sh | 47 +++++++++- .../test/contract-testing/cfn-test-create.sh | 11 +-- .../test/contract-testing/cfn-test-delete.sh | 9 +- .../test/role-policy-template.json | 17 ++++ .../test/s3-policy-template.json | 15 ++++ 7 files changed, 163 insertions(+), 31 deletions(-) create mode 100644 cfn-resources/log-integration/test/role-policy-template.json create mode 100644 cfn-resources/log-integration/test/s3-policy-template.json diff --git a/.gitignore b/.gitignore index 3a25b6477..e2a08297a 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,8 @@ CLAUDE.md # generated markdown file with resource versions, will not be commited until we have mechanism to keep updated cfn-resources/resource-versions.md + +# dynamically generated test policy files (not templates) +**/test/trust-policy.json +**/test/s3-policy.json +**/test/add-policy.json diff --git a/cfn-resources/log-integration/test/cfn-test-create-inputs.sh b/cfn-resources/log-integration/test/cfn-test-create-inputs.sh index 37f0d300a..0f8a872ca 100755 --- a/cfn-resources/log-integration/test/cfn-test-create-inputs.sh +++ b/cfn-resources/log-integration/test/cfn-test-create-inputs.sh @@ -2,6 +2,7 @@ # cfn-test-create-inputs.sh # # This tool generates json files in the inputs/ for `cfn test`. +# It creates all required AWS resources (S3 bucket, IAM role, Cloud Provider Access role) # set -o errexit @@ -9,18 +10,30 @@ set -o nounset set -o pipefail function usage { - echo "usage:$0 " - echo "Generates test input files for log integration" + echo "usage: $0 " + echo "Creates S3 bucket, Cloud Provider Access role, IAM role, and generates test input files for log integration" exit 0 } -if [ "$#" -ne 3 ]; then usage; fi +if [ "$#" -ne 1 ]; then usage; fi if [[ "$*" == help ]]; then usage; fi -rm -rf inputs -mkdir inputs +region=$AWS_DEFAULT_REGION +awsRegion=$AWS_DEFAULT_REGION +if [ -z "$region" ]; then + region=$(aws configure get region) + awsRegion=$region +fi + +regionFormatted=$(echo "$region" | sed -e "s/-/_/g" | tr '[:lower:]' '[:upper:]') +echo "Using region: $region (formatted: $regionFormatted)" + +roleName="mongodb-atlas-logs-role-${regionFormatted}" +policyName="atlas-logs-s3-policy-${regionFormatted}" +bucketTag="${CFN_TEST_TAG:-$(date +%Y%m%d%H%M%S)}" +bucketName="mongodb-atlas-cfn-test-logs-${bucketTag}" -#set profile - relevant for contract tests which define a custom profile +echo "Bucket name: ${bucketName}" profile="default" if [ ${MONGODB_ATLAS_PROFILE+x} ]; then echo "profile set to ${MONGODB_ATLAS_PROFILE}" @@ -28,9 +41,6 @@ if [ ${MONGODB_ATLAS_PROFILE+x} ]; then fi projectName="${1}" -bucketName="${2}" -iamRoleId="${3}" - 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') @@ -39,8 +49,64 @@ else echo -e "FOUND project \"${projectName}\" with id: ${projectId}\n" fi -echo "bucketName: $bucketName" -echo "iamRoleId: $iamRoleId" +echo "--------------------------------Creating Cloud Provider Access Role----------------------------" +roleID=$(atlas cloudProviders accessRoles aws create --projectId "${projectId}" --output json | jq -r '.roleId') +echo "--------------------------------Mongo CLI Role creation ends----------------------------" + +atlasAWSAccountArn=$(atlas cloudProviders accessRoles list --projectId "${projectId}" --output json | jq --arg roleID "${roleID}" -r '.awsIamRoles[] | select(.roleId == $roleID) | .atlasAWSAccountArn') +atlasAssumedRoleExternalId=$(atlas cloudProviders accessRoles list --projectId "${projectId}" --output json | jq --arg roleID "${roleID}" -r '.awsIamRoles[] | select(.roleId == $roleID) | .atlasAssumedRoleExternalId') + +jq --arg atlasAssumedRoleExternalId "$atlasAssumedRoleExternalId" \ + --arg atlasAWSAccountArn "$atlasAWSAccountArn" \ + '.Statement[0].Principal.AWS = $atlasAWSAccountArn | .Statement[0].Condition.StringEquals["sts:ExternalId"] = $atlasAssumedRoleExternalId' \ + "$(dirname "$0")/role-policy-template.json" >"$(dirname "$0")/trust-policy.json" +echo "--------------------------------AWS Role creation starts----------------------------" +awsRoleID=$(aws iam get-role --role-name "${roleName}" 2>/dev/null | jq -r '.Role.RoleId' || echo "") +if [ -z "$awsRoleID" ]; then + awsRoleID=$(aws iam create-role \ + --role-name "${roleName}" \ + --assume-role-policy-document "file://$(dirname "$0")/trust-policy.json" | jq -r '.Role.RoleId') + echo -e "No role found, hence creating the role. Created id: ${awsRoleID}\n" +else + aws iam delete-role-policy --role-name "${roleName}" --policy-name "${policyName}" 2>/dev/null || true + aws iam delete-role --role-name "${roleName}" + awsRoleID=$(aws iam create-role \ + --role-name "${roleName}" \ + --assume-role-policy-document "file://$(dirname "$0")/trust-policy.json" | jq -r '.Role.RoleId') + echo -e "FOUND role id, deleted and recreated with new trust policy. Created id: ${awsRoleID}\n" +fi +echo "--------------------------------AWS Role creation ends----------------------------" + +awsRoleArn=$(aws iam get-role --role-name "${roleName}" | jq -r '.Role.Arn') + +echo "--------------------------------Creating S3 Bucket----------------------------" +if aws s3 ls "s3://${bucketName}" 2>/dev/null; then + aws s3 rb "s3://${bucketName}" --force +fi +aws s3 mb "s3://${bucketName}" --region "${awsRegion}" +echo "Created S3 bucket: ${bucketName}" +echo "--------------------------------Attaching S3 policy to IAM role----------------------------" +bucketArn="arn:aws:s3:::${bucketName}" +jq --arg bucketArn "$bucketArn" \ + --arg bucketArnWildcard "${bucketArn}/*" \ + '.Statement[0].Resource[0] = $bucketArn | .Statement[0].Resource[1] = $bucketArnWildcard' \ + "$(dirname "$0")/s3-policy-template.json" >"$(dirname "$0")/s3-policy.json" + +aws iam put-role-policy \ + --role-name "${roleName}" \ + --policy-name "${policyName}" \ + --policy-document "file://$(dirname "$0")/s3-policy.json" +echo "--------------------------------attach mongodb Role to AWS Role ends----------------------------" + +# shellcheck disable=SC2086 +sleep 30 + +atlas cloudProviders accessRoles aws authorize "${roleID}" \ + --projectId "${projectId}" \ + --iamAssumedRoleArn "${awsRoleArn}" +echo "--------------------------------authorize mongodb Role ends----------------------------" +rm -rf inputs +mkdir inputs WORDTOREMOVE="template." cd "$(dirname "$0")" || exit @@ -48,7 +114,7 @@ for inputFile in inputs_*; do outputFile=${inputFile//$WORDTOREMOVE/} jq --arg projectId "$projectId" \ --arg bucketName "$bucketName" \ - --arg iamRoleId "$iamRoleId" \ + --arg iamRoleId "$roleID" \ --arg profile "$profile" \ '.Profile?|=$profile | .ProjectId?|=$projectId | .BucketName?|=$bucketName | .IamRoleId?|=$iamRoleId' \ "$inputFile" >"../inputs/$outputFile" diff --git a/cfn-resources/log-integration/test/cfn-test-delete-inputs.sh b/cfn-resources/log-integration/test/cfn-test-delete-inputs.sh index 3dffa7002..7fcbf44a3 100755 --- a/cfn-resources/log-integration/test/cfn-test-delete-inputs.sh +++ b/cfn-resources/log-integration/test/cfn-test-delete-inputs.sh @@ -1,12 +1,53 @@ #!/usr/bin/env bash # cfn-test-delete-inputs.sh # -# This tool deletes test input files and cleans up resources. +# This tool deletes test input files and cleans up AWS resources. # set -o errexit set -o nounset set -o pipefail -rm -rf inputs -echo "Deleted inputs directory" +function usage { + echo "usage:$0 " +} + +echo "--------------------------------delete S3 bucket and IAM role starts----------------------------" + +projectId=$(jq -r '.ProjectId' ./inputs/inputs_1_create.json) +echo "Check if a project is created $projectId" +export MCLI_PROJECT_ID=$projectId + +region=$AWS_DEFAULT_REGION +if [ -z "$region" ]; then + region=$(aws configure get region) +fi +# shellcheck disable=SC2001 +region=$(echo "$region" | sed -e "s/-/_/g") +region=$(echo "$region" | tr '[:lower:]' '[:upper:]') + +roleName="mongodb-atlas-logs-role-${region}" +policyName="atlas-logs-s3-policy-${region}" + +trustPolicy=$(jq '.Statement[0].Condition.StringEquals["sts:ExternalId"]' "$(dirname "$0")/trust-policy.json") +# shellcheck disable=SC2001 +atlasAssumedRoleExternalID=$(echo "${trustPolicy}" | sed 's/"//g') + +roleId=$(atlas cloudProviders accessRoles list --projectId "${projectId}" --output json | jq --arg roleID "${atlasAssumedRoleExternalID}" -r '.awsIamRoles[] | select(.atlasAssumedRoleExternalId | test($roleID)) | .roleId') + +atlas cloudProviders accessRoles aws deauthorize "${roleId}" --projectId "${projectId}" --force +echo "--------------------------------deauthorize role ends----------------------------" +bucketName=$(jq -r '.BucketName' "./inputs/inputs_1_create.json") +aws s3 rb "s3://${bucketName}" --force + +echo "--------------------------------delete IAM role starts----------------------------" +aws iam delete-role-policy --role-name "$roleName" --policy-name "$policyName" +aws iam delete-role --role-name "$roleName" +echo "--------------------------------delete IAM role ends----------------------------" + +#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/log-integration/test/contract-testing/cfn-test-create.sh b/cfn-resources/log-integration/test/contract-testing/cfn-test-create.sh index 515eec0fa..8208f92c5 100755 --- a/cfn-resources/log-integration/test/contract-testing/cfn-test-create.sh +++ b/cfn-resources/log-integration/test/contract-testing/cfn-test-create.sh @@ -6,15 +6,10 @@ set -o nounset set -o pipefail projectName="cfn-test-bot-$(date +%s)-$RANDOM" -bucketName="atlas-logs-cfn-test-$RANDOM" -iamRoleId="65a1b2c3d4e5f6a7b8c9d0e1" -# create project -projectId=$(atlas projects create "${projectName}" --output=json | jq -r '.id') +# Set unique tag for S3 bucket to avoid conflicts in CI +export CFN_TEST_TAG="${projectName}" -echo "projectId: $projectId" echo "projectName: $projectName" -echo "bucketName: $bucketName" -echo "iamRoleId: $iamRoleId (dummy 24-char hex format)" -./test/cfn-test-create-inputs.sh "$projectName" "$bucketName" "$iamRoleId" +./test/cfn-test-create-inputs.sh "$projectName" diff --git a/cfn-resources/log-integration/test/contract-testing/cfn-test-delete.sh b/cfn-resources/log-integration/test/contract-testing/cfn-test-delete.sh index 71286ddfb..a1e063f38 100755 --- a/cfn-resources/log-integration/test/contract-testing/cfn-test-delete.sh +++ b/cfn-resources/log-integration/test/contract-testing/cfn-test-delete.sh @@ -5,11 +5,4 @@ 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 +./test/cfn-test-delete-inputs.sh diff --git a/cfn-resources/log-integration/test/role-policy-template.json b/cfn-resources/log-integration/test/role-policy-template.json new file mode 100644 index 000000000..7a579aa97 --- /dev/null +++ b/cfn-resources/log-integration/test/role-policy-template.json @@ -0,0 +1,17 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "" + } + } + } + ] +} diff --git a/cfn-resources/log-integration/test/s3-policy-template.json b/cfn-resources/log-integration/test/s3-policy-template.json new file mode 100644 index 000000000..c8f500beb --- /dev/null +++ b/cfn-resources/log-integration/test/s3-policy-template.json @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:PutObject", + "s3:GetObject", + "s3:ListBucket", + "s3:GetBucketLocation" + ], + "Resource": ["", ""] + } + ] +} From dce61426b271fcd9e3851e28550d5acb525d3620 Mon Sep 17 00:00:00 2001 From: Rakhul S Prakash Date: Wed, 11 Feb 2026 10:02:17 +0530 Subject: [PATCH 3/3] test --- .gitignore | 1 + .../test/cfn-test-create-inputs.sh | 12 ++- .../test/cfn-test-delete-inputs.sh | 83 ++++++++++++++----- 3 files changed, 76 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index e2a08297a..7ce7ac631 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ cfn-resources/resource-versions.md **/test/trust-policy.json **/test/s3-policy.json **/test/add-policy.json +**/test/test-metadata.json diff --git a/cfn-resources/log-integration/test/cfn-test-create-inputs.sh b/cfn-resources/log-integration/test/cfn-test-create-inputs.sh index 0f8a872ca..832adcc56 100755 --- a/cfn-resources/log-integration/test/cfn-test-create-inputs.sh +++ b/cfn-resources/log-integration/test/cfn-test-create-inputs.sh @@ -28,7 +28,8 @@ fi regionFormatted=$(echo "$region" | sed -e "s/-/_/g" | tr '[:lower:]' '[:upper:]') echo "Using region: $region (formatted: $regionFormatted)" -roleName="mongodb-atlas-logs-role-${regionFormatted}" +# Use dynamic role name to avoid conflicts in CI (matches test-folder pattern) +roleName="mongodb-atlas-logs-role-${regionFormatted}-$(date +%s)-${RANDOM}" policyName="atlas-logs-s3-policy-${regionFormatted}" bucketTag="${CFN_TEST_TAG:-$(date +%Y%m%d%H%M%S)}" bucketName="mongodb-atlas-cfn-test-logs-${bucketTag}" @@ -108,6 +109,15 @@ echo "--------------------------------authorize mongodb Role ends--------------- rm -rf inputs mkdir inputs +# Store AWS role ARN in a separate metadata file for cleanup (not part of CFN schema) +cat > "$(dirname "$0")/test-metadata.json" < /tmp/atlas_roles.json 2>&1; then + roleId=$(jq --arg roleID "${atlasAssumedRoleExternalID}" -r '.awsIamRoles[] | select(.atlasAssumedRoleExternalId | test($roleID)) | .roleId' /tmp/atlas_roles.json 2>/dev/null || echo "") + rm -f /tmp/atlas_roles.json -atlas cloudProviders accessRoles aws deauthorize "${roleId}" --projectId "${projectId}" --force -echo "--------------------------------deauthorize role ends----------------------------" + if [ -n "${roleId}" ] && [ "${roleId}" != "null" ] && [ "${roleId}" != "" ]; then + echo "Deauthorizing role from Atlas: ${roleId}" + if atlas cloudProviders accessRoles aws deauthorize "${roleId}" --projectId "${projectId}" --force; then + echo "Successfully deauthorized role" + else + echo "Failed to deauthorize role (may already be deauthorized)" + fi + echo "--------------------------------deauthorize role ends----------------------------" + else + echo "Warning: Could not find Atlas role ID to deauthorize (may already be deauthorized)" + fi + else + echo "Warning: Could not list Atlas roles (may be authentication issue or project already deleted)" + rm -f /tmp/atlas_roles.json + fi +else + if [ -z "$roleArn" ]; then + echo "Warning: No role ARN found in metadata, skipping Atlas role deauthorization" + else + echo "Warning: trust-policy.json not found, skipping Atlas role deauthorization" + fi +fi bucketName=$(jq -r '.BucketName' "./inputs/inputs_1_create.json") -aws s3 rb "s3://${bucketName}" --force +if [ -n "$bucketName" ] && [ "$bucketName" != "null" ]; then + echo "Deleting S3 bucket: ${bucketName}" + aws s3 rb "s3://${bucketName}" --force || echo "Failed to delete S3 bucket (may already be deleted)" +else + echo "Warning: No bucket name found in inputs" +fi echo "--------------------------------delete IAM role starts----------------------------" -aws iam delete-role-policy --role-name "$roleName" --policy-name "$policyName" -aws iam delete-role --role-name "$roleName" +if [ -n "$roleName" ]; then + echo "Deleting IAM role: ${roleName}" + aws iam delete-role-policy --role-name "$roleName" --policy-name "$policyName" 2>/dev/null || echo "Role policy already deleted or doesn't exist" + aws iam delete-role --role-name "$roleName" 2>/dev/null || echo "Role already deleted or doesn't exist" + echo "Deleted IAM role: ${roleName}" +else + echo "No IAM role to delete (not found in metadata)" +fi echo "--------------------------------delete IAM role ends----------------------------" +# Clean up temporary test files +rm -f "$(dirname "$0")/trust-policy.json" +rm -f "$(dirname "$0")/s3-policy.json" +rm -f "$(dirname "$0")/test-metadata.json" +echo "Cleaned up temporary test files" + #delete project if atlas projects delete "$projectId" --force; then echo "$projectId project deletion OK" else - (echo "Failed cleaning project:$projectId" && exit 1) + echo "Warning: Failed cleaning project:$projectId (may be authentication issue or already deleted)" fi