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/.gitignore b/.gitignore index 3a25b6477..7ce7ac631 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,9 @@ 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 +**/test/test-metadata.json 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..832adcc56 --- /dev/null +++ b/cfn-resources/log-integration/test/cfn-test-create-inputs.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# cfn-test-create-inputs.sh +# +# This tool generates json files in the inputs/ for `cfn test`. +# It creates all required AWS resources (S3 bucket, IAM role, Cloud Provider Access role) +# + +set -o errexit +set -o nounset +set -o pipefail + +function usage { + echo "usage: $0