diff --git a/.github/workflows/contract-testing.yaml b/.github/workflows/contract-testing.yaml index 8ae429783..4820758d1 100644 --- a/.github/workflows/contract-testing.yaml +++ b/.github/workflows/contract-testing.yaml @@ -30,6 +30,7 @@ jobs: search-deployment: ${{ steps.filter.outputs.search-deployment }} stream-connection: ${{ steps.filter.outputs.stream-connection }} stream-instance: ${{ steps.filter.outputs.stream-instance }} + stream-workspace: ${{ steps.filter.outputs.stream-workspace }} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 if: ${{ github.event_name == 'push' }} @@ -75,6 +76,8 @@ jobs: - 'cfn-resources/stream-connection/**' stream-instance: - 'cfn-resources/stream-instance/**' + stream-workspace: + - 'cfn-resources/stream-workspace/**' access-list-api-key: needs: change-detection if: ${{ needs.change-detection.outputs.access-list-api-key == 'true' }} @@ -854,3 +857,44 @@ jobs: cat inputs/inputs_1_create.json make run-contract-testing make delete-test-resources + stream-workspace: + needs: change-detection + if: ${{ needs.change-detection.outputs.stream-workspace == 'true' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c + with: + go-version-file: 'cfn-resources/go.mod' + - name: setup Atlas CLI + uses: mongodb/atlas-github-action@e3c9e0204659bafbb3b65e1eb1ee745cca0e9f3b + - uses: aws-actions/setup-sam@c2a20b1822cc4a6bc594ff7f1dbb658758e383c3 + with: + use-installer: true + - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_TEST_ENV }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_TEST_ENV }} + aws-region: eu-west-1 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 + with: + python-version: '3.9' + cache: 'pip' # caching pip dependencies + - run: pip install cloudformation-cli cloudformation-cli-go-plugin + - name: Run the Contract test + shell: bash + env: + MONGODB_ATLAS_PUBLIC_API_KEY: ${{ secrets.CLOUD_DEV_PUBLIC_KEY }} + MONGODB_ATLAS_PRIVATE_API_KEY: ${{ secrets.CLOUD_DEV_PRIVATE_KEY }} + MONGODB_ATLAS_ORG_ID: ${{ secrets.CLOUD_DEV_ORG_ID }} + MONGODB_ATLAS_OPS_MANAGER_URL: ${{ vars.MONGODB_ATLAS_BASE_URL }} + MONGODB_ATLAS_PROFILE: cfn-cloud-dev-github-action + run: | + cd cfn-resources/stream-workspace + make create-test-resources + + cat inputs/inputs_1_create.json + cat inputs/inputs_1_update.json + + make run-contract-testing + make delete-test-resources \ No newline at end of file diff --git a/cfn-resources/stream-workspace/.rpdk-config b/cfn-resources/stream-workspace/.rpdk-config new file mode 100644 index 000000000..71b0b45c9 --- /dev/null +++ b/cfn-resources/stream-workspace/.rpdk-config @@ -0,0 +1,22 @@ +{ + "artifact_type": "RESOURCE", + "typeName": "MongoDB::Atlas::StreamWorkspace", + "language": "go", + "runtime": "provided.al2", + "entrypoint": "bootstrap", + "testEntrypoint": "bootstrap", + "settings": { + "version": false, + "subparser_name": null, + "verbose": 0, + "force": false, + "type_name": "MongoDB::Atlas::StreamWorkspace", + "artifact_type": null, + "endpoint_url": null, + "region": null, + "target_schemas": [], + "profile": null, + "import_path": "github.com/mongodb/mongodbatlas-cloudformation-resources/stream-workspace", + "protocolVersion": "2.0.0" + } +} diff --git a/cfn-resources/stream-workspace/Makefile b/cfn-resources/stream-workspace/Makefile new file mode 100644 index 000000000..296ff567e --- /dev/null +++ b/cfn-resources/stream-workspace/Makefile @@ -0,0 +1,33 @@ +.PHONY: build test clean +tags=logging callback metrics scheduler +cgo=0 +goos=linux +goarch=amd64 +CFNREP_GIT_SHA?=$(shell git rev-parse HEAD) +ldXflags=-s -w -X github.com/mongodb/mongodbatlas-cloudformation-resources/util.defaultLogLevel=info -X github.com/mongodb/mongodbatlas-cloudformation-resources/version.Version=${CFNREP_GIT_SHA} +ldXflagsD=-X github.com/mongodb/mongodbatlas-cloudformation-resources/util.defaultLogLevel=debug -X github.com/mongodb/mongodbatlas-cloudformation-resources/version.Version=${CFNREP_GIT_SHA} + +build: + cfn generate + env GOOS=$(goos) CGO_ENABLED=$(cgo) GOARCH=$(goarch) go build -ldflags="$(ldXflags)" -tags="$(tags)" -o bin/bootstrap cmd/main.go + +debug: + cfn generate + env GOOS=$(goos) CGO_ENABLED=$(cgo) GOARCH=$(goarch) go build -ldflags="$(ldXflagsD)" -tags="$(tags)" -o bin/bootstrap cmd/main.go + +clean: + rm -rf bin + +create-test-resources: + @echo "==> Creating test files for contract testing" + ./test/contract-testing/cfn-test-create-inputs.sh + +delete-test-resources: + @echo "==> Delete test resources used for contract testing" + ./test/cfn-test-delete-inputs.sh + +run-contract-testing: + @echo "==> Run contract testing" + make build + sam local start-lambda & + cfn test --function-name TestEntrypoint --verbose diff --git a/cfn-resources/stream-workspace/README.md b/cfn-resources/stream-workspace/README.md new file mode 100644 index 000000000..484467009 --- /dev/null +++ b/cfn-resources/stream-workspace/README.md @@ -0,0 +1,18 @@ +# MongoDB::Atlas::StreamWorkspace + +## Description + +Resource for managing [Stream Workspaces](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/group/endpoint-streams). + +## Requirements + +Set up an AWS profile to securely give CloudFormation access to your Atlas credentials. +For instructions on setting up a profile, [see here](/README.md#mongodb-atlas-api-keys-credential-management). + +## Attributes and Parameters + +See the [resource docs](docs/README.md). Also refer [AWS security best practices for CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/security-best-practices.html#creds) to manage credentials. + +## CloudFormation Examples + +See the examples [CFN Template](/examples/stream-workspace/stream-workspace.json) for example resource. diff --git a/cfn-resources/stream-workspace/cmd/main.go b/cfn-resources/stream-workspace/cmd/main.go new file mode 100644 index 000000000..b0feb978b --- /dev/null +++ b/cfn-resources/stream-workspace/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/stream-workspace/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/stream-workspace/cmd/resource/config.go b/cfn-resources/stream-workspace/cmd/resource/config.go new file mode 100644 index 000000000..4d9eb7831 --- /dev/null +++ b/cfn-resources/stream-workspace/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/stream-workspace/cmd/resource/mappings.go b/cfn-resources/stream-workspace/cmd/resource/mappings.go new file mode 100644 index 000000000..c872f5976 --- /dev/null +++ b/cfn-resources/stream-workspace/cmd/resource/mappings.go @@ -0,0 +1,98 @@ +// Copyright 2026 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package resource + +import "go.mongodb.org/atlas-sdk/v20250312012/admin" + +func NewStreamWorkspaceCreateReq(model *Model) *admin.StreamsTenant { + if model == nil { + return nil + } + dataProcessRegion := *model.DataProcessRegion + streamTenant := &admin.StreamsTenant{ + Name: model.WorkspaceName, + GroupId: model.ProjectId, + DataProcessRegion: &admin.StreamsDataProcessRegion{ + CloudProvider: *dataProcessRegion.CloudProvider, + Region: *dataProcessRegion.Region, + }, + } + if streamConfig := model.StreamConfig; streamConfig != nil { + streamTenant.StreamConfig = &admin.StreamConfig{} + if tier := streamConfig.Tier; tier != nil { + streamTenant.StreamConfig.Tier = tier + } + if maxTierSize := streamConfig.MaxTierSize; maxTierSize != nil { + streamTenant.StreamConfig.MaxTierSize = maxTierSize + } + } + return streamTenant +} + +func NewStreamWorkspaceUpdateReq(model *Model) *admin.StreamsTenantUpdateRequest { + if model == nil || model.DataProcessRegion == nil { + return nil + } + dataProcessRegion := *model.DataProcessRegion + if dataProcessRegion.Region == nil { + return nil + } + // CloudFormation is AWS-only, so CloudProvider is always AWS + cloudProvider := CloudProvider + return &admin.StreamsTenantUpdateRequest{ + CloudProvider: &cloudProvider, + Region: dataProcessRegion.Region, + } +} + +func newModelDataRegion(dataProcessRegion *admin.StreamsDataProcessRegion) *StreamsDataProcessRegion { + return &StreamsDataProcessRegion{ + CloudProvider: &dataProcessRegion.CloudProvider, + Region: &dataProcessRegion.Region, + } +} + +func newModelStreamConfig(streamConfig *admin.StreamConfig) *StreamConfig { + if streamConfig == nil { + return nil + } + modelConfig := &StreamConfig{} + if streamConfig.Tier != nil { + modelConfig.Tier = streamConfig.Tier + } + if streamConfig.MaxTierSize != nil { + modelConfig.MaxTierSize = streamConfig.MaxTierSize + } + return modelConfig +} + +func GetStreamWorkspaceModel(streamTenant *admin.StreamsTenant, currentModel *Model) *Model { + model := new(Model) + + if currentModel != nil { + model = currentModel + } + + if streamTenant != nil { + model.WorkspaceName = streamTenant.Name + model.DataProcessRegion = newModelDataRegion(streamTenant.DataProcessRegion) + model.StreamConfig = newModelStreamConfig(streamTenant.StreamConfig) + model.ProjectId = streamTenant.GroupId + model.Id = streamTenant.Id + model.Hostnames = streamTenant.GetHostnames() + } + + return model +} diff --git a/cfn-resources/stream-workspace/cmd/resource/mappings_test.go b/cfn-resources/stream-workspace/cmd/resource/mappings_test.go new file mode 100644 index 000000000..3ab17861a --- /dev/null +++ b/cfn-resources/stream-workspace/cmd/resource/mappings_test.go @@ -0,0 +1,184 @@ +// 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/stream-workspace/cmd/resource" + "github.com/stretchr/testify/assert" + "go.mongodb.org/atlas-sdk/v20250312012/admin" +) + +var ( + workspaceName = "name" + projectID = "projectId" + cloudProvider = resource.CloudProvider + region = "VIRGINIA_USA" + tier = "SP30" + maxTierSize = "SP50" +) + +func TestNewStreamWorkspaceCreateReq(t *testing.T) { + testCases := []struct { + input *resource.Model + expected *admin.StreamsTenant + name string + }{ + { + name: "Model with StreamConfig including Tier and MaxTierSize", + input: &resource.Model{ + WorkspaceName: &workspaceName, + ProjectId: &projectID, + DataProcessRegion: &resource.StreamsDataProcessRegion{ + CloudProvider: &cloudProvider, + Region: ®ion, + }, + StreamConfig: &resource.StreamConfig{ + Tier: &tier, + MaxTierSize: &maxTierSize, + }, + }, + expected: &admin.StreamsTenant{ + Name: &workspaceName, + GroupId: &projectID, + DataProcessRegion: &admin.StreamsDataProcessRegion{ + CloudProvider: cloudProvider, + Region: region, + }, + StreamConfig: &admin.StreamConfig{ + Tier: &tier, + MaxTierSize: &maxTierSize, + }, + }, + }, + { + name: "Model with StreamConfig with only Tier", + input: &resource.Model{ + WorkspaceName: &workspaceName, + ProjectId: &projectID, + DataProcessRegion: &resource.StreamsDataProcessRegion{ + CloudProvider: &cloudProvider, + Region: ®ion, + }, + StreamConfig: &resource.StreamConfig{ + Tier: &tier, + }, + }, + expected: &admin.StreamsTenant{ + Name: &workspaceName, + GroupId: &projectID, + DataProcessRegion: &admin.StreamsDataProcessRegion{ + CloudProvider: cloudProvider, + Region: region, + }, + StreamConfig: &admin.StreamConfig{ + Tier: &tier, + }, + }, + }, + { + name: "Model without StreamConfig", + input: &resource.Model{ + WorkspaceName: &workspaceName, + ProjectId: &projectID, + DataProcessRegion: &resource.StreamsDataProcessRegion{ + CloudProvider: &cloudProvider, + Region: ®ion, + }, + }, + expected: &admin.StreamsTenant{ + Name: &workspaceName, + GroupId: &projectID, + DataProcessRegion: &admin.StreamsDataProcessRegion{ + CloudProvider: cloudProvider, + Region: region, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := resource.NewStreamWorkspaceCreateReq(tc.input) + assert.Equal(t, tc.expected, result, "created model did not match expected output") + }) + } +} + +func TestNewStreamWorkspaceUpdateReq(t *testing.T) { + newRegion := "OREGON_USA" + awsProvider := resource.CloudProvider + testCases := []struct { + input *resource.Model + expected *admin.StreamsTenantUpdateRequest + name string + }{ + { + name: "Model with DataProcessRegion and Region", + input: &resource.Model{ + DataProcessRegion: &resource.StreamsDataProcessRegion{ + CloudProvider: &cloudProvider, + Region: &newRegion, + }, + }, + expected: &admin.StreamsTenantUpdateRequest{ + CloudProvider: &awsProvider, + Region: &newRegion, + }, + }, + { + name: "Model with DataProcessRegion but no CloudProvider (should still work)", + input: &resource.Model{ + DataProcessRegion: &resource.StreamsDataProcessRegion{ + Region: &newRegion, + }, + }, + expected: &admin.StreamsTenantUpdateRequest{ + CloudProvider: &awsProvider, + Region: &newRegion, + }, + }, + { + name: "Model is nil", + input: nil, + expected: nil, + }, + { + name: "Model with nil DataProcessRegion", + input: &resource.Model{ + DataProcessRegion: nil, + }, + expected: nil, + }, + { + name: "Model with DataProcessRegion but nil Region", + input: &resource.Model{ + DataProcessRegion: &resource.StreamsDataProcessRegion{ + CloudProvider: &cloudProvider, + Region: nil, + }, + }, + expected: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := resource.NewStreamWorkspaceUpdateReq(tc.input) + assert.Equal(t, tc.expected, result, "update request did not match expected output") + }) + } +} diff --git a/cfn-resources/stream-workspace/cmd/resource/model.go b/cfn-resources/stream-workspace/cmd/resource/model.go new file mode 100644 index 000000000..da4de0e4c --- /dev/null +++ b/cfn-resources/stream-workspace/cmd/resource/model.go @@ -0,0 +1,26 @@ +// 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"` + WorkspaceName *string `json:",omitempty"` + DataProcessRegion *StreamsDataProcessRegion `json:",omitempty"` + StreamConfig *StreamConfig `json:",omitempty"` + Id *string `json:",omitempty"` + ProjectId *string `json:",omitempty"` + Hostnames []string `json:",omitempty"` +} + +// StreamsDataProcessRegion is autogenerated from the json schema +type StreamsDataProcessRegion struct { + CloudProvider *string `json:",omitempty"` + Region *string `json:",omitempty"` +} + +// StreamConfig is autogenerated from the json schema +type StreamConfig struct { + Tier *string `json:",omitempty"` + MaxTierSize *string `json:",omitempty"` +} diff --git a/cfn-resources/stream-workspace/cmd/resource/resource.go b/cfn-resources/stream-workspace/cmd/resource/resource.go new file mode 100644 index 000000000..7fe4f1524 --- /dev/null +++ b/cfn-resources/stream-workspace/cmd/resource/resource.go @@ -0,0 +1,213 @@ +// 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/aws/aws-sdk-go-v2/service/cloudformation/types" + + "github.com/mongodb/mongodbatlas-cloudformation-resources/util" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/constants" + progress_events "github.com/mongodb/mongodbatlas-cloudformation-resources/util/progressevent" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/validator" + "go.mongodb.org/atlas-sdk/v20250312012/admin" +) + +var CreateRequiredFields = []string{"WorkspaceName", constants.ProjectID, constants.DataProcessRegion} +var ReadRequiredFields = []string{"WorkspaceName", constants.ProjectID} +var UpdateRequiredFields = []string{"WorkspaceName", constants.ProjectID, constants.DataProcessRegion} +var DeleteRequiredFields = []string{"WorkspaceName", constants.ProjectID} +var ListRequiredFields = []string{constants.ProjectID} + +const ( + CloudProvider = "AWS" + DefaultItemsPerPage = 100 +) + +var InitEnvWithLatestClient = func(req handler.Request, currentModel *Model, requiredFields []string) (*admin.APIClient, *handler.ProgressEvent) { + util.SetupLogger("mongodb-atlas-stream-workspace") + + util.SetDefaultProfileIfNotDefined(¤tModel.Profile) + + if errEvent := validator.ValidateModel(requiredFields, currentModel); errEvent != nil { + return nil, errEvent + } + + client, peErr := util.NewAtlasClient(&req, currentModel.Profile) + if peErr != nil { + return nil, peErr + } + return client.AtlasSDK, nil +} + +func Create(req handler.Request, prevModel *Model, currentModel *Model) (handler.ProgressEvent, error) { + conn, peErr := InitEnvWithLatestClient(req, currentModel, CreateRequiredFields) + if peErr != nil { + return *peErr, nil + } + + ctx := context.Background() + + streamWorkspaceCreateReq := NewStreamWorkspaceCreateReq(currentModel) + + createdStreamWorkspace, resp, err := conn.StreamsApi.CreateStreamWorkspace(ctx, *currentModel.ProjectId, streamWorkspaceCreateReq).Execute() + if err != nil { + return HandleError(resp, constants.CREATE, err) + } + + model := GetStreamWorkspaceModel(createdStreamWorkspace, currentModel) + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: "Create Completed", + ResourceModel: model, + }, nil +} + +func Read(req handler.Request, prevModel *Model, currentModel *Model) (handler.ProgressEvent, error) { + conn, peErr := InitEnvWithLatestClient(req, currentModel, ReadRequiredFields) + if peErr != nil { + return *peErr, nil + } + + ctx := context.Background() + + streamWorkspace, resp, err := conn.StreamsApi.GetStreamWorkspace(ctx, *currentModel.ProjectId, *currentModel.WorkspaceName).Execute() + if err != nil { + if util.StatusNotFound(resp) { + return handler.ProgressEvent{ + OperationStatus: handler.Failed, + Message: "StreamWorkspace not found", + HandlerErrorCode: string(types.HandlerErrorCodeNotFound), + }, nil + } + return HandleError(resp, constants.READ, err) + } + + model := GetStreamWorkspaceModel(streamWorkspace, currentModel) + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.ReadComplete, + ResourceModel: model, + }, nil +} + +func Update(req handler.Request, prevModel *Model, currentModel *Model) (handler.ProgressEvent, error) { + conn, peErr := InitEnvWithLatestClient(req, currentModel, UpdateRequiredFields) + if peErr != nil { + return *peErr, nil + } + + ctx := context.Background() + + streamWorkspaceUpdateReq := NewStreamWorkspaceUpdateReq(currentModel) + updatedStreamWorkspace, resp, err := conn.StreamsApi.UpdateStreamWorkspace(ctx, *currentModel.ProjectId, *currentModel.WorkspaceName, streamWorkspaceUpdateReq).Execute() + if err != nil { + if util.StatusNotFound(resp) { + return handler.ProgressEvent{ + OperationStatus: handler.Failed, + Message: "StreamWorkspace not found", + HandlerErrorCode: string(types.HandlerErrorCodeNotFound), + }, nil + } + return HandleError(resp, constants.UPDATE, err) + } + + model := GetStreamWorkspaceModel(updatedStreamWorkspace, currentModel) + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: "Update Completed", + ResourceModel: model, + }, nil +} + +func Delete(req handler.Request, prevModel *Model, currentModel *Model) (handler.ProgressEvent, error) { + conn, peErr := InitEnvWithLatestClient(req, currentModel, DeleteRequiredFields) + if peErr != nil { + return *peErr, nil + } + + ctx := context.Background() + + resp, err := conn.StreamsApi.DeleteStreamWorkspace(ctx, *currentModel.ProjectId, *currentModel.WorkspaceName).Execute() + if err != nil { + return HandleError(resp, constants.DELETE, err) + } + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: "Delete Completed", + }, nil +} + +func List(req handler.Request, prevModel *Model, currentModel *Model) (handler.ProgressEvent, error) { + conn, peErr := InitEnvWithLatestClient(req, currentModel, ListRequiredFields) + if peErr != nil { + return *peErr, nil + } + + ctx := context.Background() + + accumulatedStreamWorkspaces, apiResp, err := getAllStreamWorkspaces(ctx, conn, *currentModel.ProjectId) + if err != nil { + return HandleError(apiResp, constants.LIST, err) + } + + response := make([]interface{}, 0) + for i := range accumulatedStreamWorkspaces { + model := GetStreamWorkspaceModel(&accumulatedStreamWorkspaces[i], nil) + model.ProjectId = currentModel.ProjectId + model.Profile = currentModel.Profile + response = append(response, model) + } + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.Complete, + ResourceModels: response, + }, nil +} + +func getAllStreamWorkspaces(ctx context.Context, conn *admin.APIClient, projectID string) ([]admin.StreamsTenant, *http.Response, error) { + pageNum := 1 + accumulatedStreamWorkspaces := make([]admin.StreamsTenant, 0) + + for allStreamWorkspacesRetrieved := false; !allStreamWorkspacesRetrieved; { + streamWorkspaces, resp, err := conn.StreamsApi.ListStreamWorkspacesWithParams(ctx, &admin.ListStreamWorkspacesApiParams{ + GroupId: projectID, + ItemsPerPage: util.Pointer(DefaultItemsPerPage), + PageNum: util.Pointer(pageNum), + }).Execute() + + if err != nil { + return nil, resp, err + } + accumulatedStreamWorkspaces = append(accumulatedStreamWorkspaces, streamWorkspaces.GetResults()...) + allStreamWorkspacesRetrieved = streamWorkspaces.GetTotalCount() <= len(accumulatedStreamWorkspaces) + pageNum++ + } + + return accumulatedStreamWorkspaces, nil, nil +} + +func HandleError(response *http.Response, method constants.CfnFunctions, err error) (handler.ProgressEvent, error) { + errMsg := fmt.Sprintf("%s error:%s", method, err.Error()) + return progress_events.GetFailedEventByResponse(errMsg, response), nil +} diff --git a/cfn-resources/stream-workspace/docs/README.md b/cfn-resources/stream-workspace/docs/README.md new file mode 100644 index 000000000..ee90cc770 --- /dev/null +++ b/cfn-resources/stream-workspace/docs/README.md @@ -0,0 +1,103 @@ +# MongoDB::Atlas::StreamWorkspace + +Returns, adds, updates, and removes Atlas Stream Processing Workspaces. The DataProcessRegion.Region property can be updated after creation. Other properties (WorkspaceName, ProjectId, Profile, StreamConfig, DataProcessRegion.CloudProvider) are create-only and require resource replacement to change. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "MongoDB::Atlas::StreamWorkspace",
+    "Properties" : {
+        "Profile" : String,
+        "WorkspaceName" : String,
+        "DataProcessRegion" : StreamsDataProcessRegion,
+        "StreamConfig" : StreamConfig,
+        "ProjectId" : String,
+    }
+}
+
+ +### YAML + +
+Type: MongoDB::Atlas::StreamWorkspace
+Properties:
+    Profile: String
+    WorkspaceName: String
+    DataProcessRegion: StreamsDataProcessRegion
+    StreamConfig: StreamConfig
+    ProjectId: String
+
+ +## Properties + +#### Profile + +The profile is defined in AWS Secret manager. See [Secret Manager Profile setup](../../../examples/profile-secret.yaml). + +_Required_: No + +_Type_: String + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### WorkspaceName + +Human-readable label that identifies the stream workspace. + +_Required_: Yes + +_Type_: String + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### DataProcessRegion + +Information about the cloud provider region in which MongoDB Cloud processes the stream. + +_Required_: Yes + +_Type_: StreamsDataProcessRegion + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### StreamConfig + +Configuration options for an Atlas Stream Processing Workspace. + +_Required_: No + +_Type_: StreamConfig + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### ProjectId + +Unique 24-hexadecimal character string that identifies the project. + +_Required_: Yes + +_Type_: String + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +## 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). + +#### Id + +Unique 24-hexadecimal character string that identifies the stream workspace. + +#### Hostnames + +List that contains the hostnames assigned to the stream workspace. + diff --git a/cfn-resources/stream-workspace/docs/streamconfig.md b/cfn-resources/stream-workspace/docs/streamconfig.md new file mode 100644 index 000000000..600c9f6e9 --- /dev/null +++ b/cfn-resources/stream-workspace/docs/streamconfig.md @@ -0,0 +1,50 @@ +# MongoDB::Atlas::StreamWorkspace StreamConfig + +Configuration options for an Atlas Stream Processing Workspace. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Tier" : String,
+    "MaxTierSize" : String
+}
+
+ +### YAML + +
+Tier: String
+MaxTierSize: String
+
+ +## Properties + +#### Tier + +Selected tier for the Stream Workspace. Configures Memory / VCPU allowances. + +_Required_: Yes + +_Type_: String + +_Allowed Values_: SP2 | SP5 | SP10 | SP30 | SP50 + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### MaxTierSize + +Max tier size for the Stream Workspace. Configures Memory / VCPU allowances. + +_Required_: No + +_Type_: String + +_Allowed Values_: SP2 | SP5 | SP10 | SP30 | SP50 + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + diff --git a/cfn-resources/stream-workspace/docs/streamsdataprocessregion.md b/cfn-resources/stream-workspace/docs/streamsdataprocessregion.md new file mode 100644 index 000000000..ab6bf186a --- /dev/null +++ b/cfn-resources/stream-workspace/docs/streamsdataprocessregion.md @@ -0,0 +1,48 @@ +# MongoDB::Atlas::StreamWorkspace StreamsDataProcessRegion + +Information about the cloud provider region in which MongoDB Cloud processes the stream. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "CloudProvider" : String,
+    "Region" : String
+}
+
+ +### YAML + +
+CloudProvider: String
+Region: String
+
+ +## Properties + +#### CloudProvider + +Label that identifies the cloud service provider where MongoDB Cloud performs stream processing. For CloudFormation, this is restricted to AWS only. + +_Required_: Yes + +_Type_: String + +_Allowed Values_: AWS + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Region + +Name of the cloud provider region hosting Atlas Stream Processing. + +_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) + diff --git a/cfn-resources/stream-workspace/mongodb-atlas-streamworkspace.json b/cfn-resources/stream-workspace/mongodb-atlas-streamworkspace.json new file mode 100644 index 000000000..6e58a8eb5 --- /dev/null +++ b/cfn-resources/stream-workspace/mongodb-atlas-streamworkspace.json @@ -0,0 +1,117 @@ +{ + "typeName": "MongoDB::Atlas::StreamWorkspace", + "description": "Returns, adds, updates, and removes Atlas Stream Processing Workspaces. The DataProcessRegion.Region property can be updated after creation. Other properties (WorkspaceName, ProjectId, Profile, StreamConfig, DataProcessRegion.CloudProvider) are create-only and require resource replacement to change.", + "sourceUrl": "https://github.com/mongodb/mongodbatlas-cloudformation-resources.git", + "definitions": { + "BaseStreamsRegion": { + "type": "string", + "description": "Name of the cloud provider region hosting Atlas Stream Processing." + }, + "StreamsDataProcessRegion": { + "type": "object", + "description": "Information about the cloud provider region in which MongoDB Cloud processes the stream.", + "properties": { + "CloudProvider": { + "type": "string", + "description": "Label that identifies the cloud service provider where MongoDB Cloud performs stream processing. For CloudFormation, this is restricted to AWS only.", + "enum": ["AWS"], + "default": "AWS" + }, + "Region": { + "$ref": "#/definitions/BaseStreamsRegion" + } + }, + "required": ["CloudProvider", "Region"], + "additionalProperties": false + }, + "StreamConfig": { + "type": "object", + "description": "Configuration options for an Atlas Stream Processing Workspace.", + "properties": { + "Tier": { + "type": "string", + "description": "Selected tier for the Stream Workspace. Configures Memory / VCPU allowances. Valid values: SP2, SP5, SP10, SP30, SP50.", + "title": "Stream Workspace Tier" + }, + "MaxTierSize": { + "type": "string", + "description": "Max tier size for the Stream Workspace. Configures Memory / VCPU allowances. Valid values: SP2, SP5, SP10, SP30, SP50.", + "title": "Stream Workspace Max Tier Size" + } + }, + "required": ["Tier"], + "additionalProperties": false + } + }, + "properties": { + "Profile": { + "type": "string", + "description": "The profile is defined in AWS Secret manager. See [Secret Manager Profile setup](../../../examples/profile-secret.yaml).", + "default": "default" + }, + "WorkspaceName": { + "description": "Human-readable label that identifies the stream workspace.", + "type": "string" + }, + "DataProcessRegion": { + "$ref": "#/definitions/StreamsDataProcessRegion" + }, + "StreamConfig": { + "$ref": "#/definitions/StreamConfig" + }, + "Id": { + "description": "Unique 24-hexadecimal character string that identifies the stream workspace.", + "type": "string" + }, + "ProjectId": { + "description": "Unique 24-hexadecimal character string that identifies the project.", + "type": "string" + }, + "Hostnames": { + "description": "List that contains the hostnames assigned to the stream workspace.", + "type": "array", + "items": { + "type": "string" + }, + "insertionOrder": false + } + }, + "additionalProperties": false, + "required": ["ProjectId", "WorkspaceName", "DataProcessRegion"], + "readOnlyProperties": [ + "/properties/Id", + "/properties/Hostnames" + ], + "primaryIdentifier": [ + "/properties/WorkspaceName", + "/properties/ProjectId", + "/properties/Profile" + ], + "createOnlyProperties": [ + "/properties/WorkspaceName", + "/properties/ProjectId", + "/properties/Profile", + "/properties/StreamConfig" + ], + "handlers": { + "create": { + "permissions": ["secretsmanager:GetSecretValue"] + }, + "read": { + "permissions": ["secretsmanager:GetSecretValue"] + }, + "update": { + "permissions": ["secretsmanager:GetSecretValue"] + }, + "delete": { + "permissions": ["secretsmanager:GetSecretValue"] + }, + "list": { + "permissions": ["secretsmanager:GetSecretValue"] + } + }, + "documentationUrl": "https://github.com/mongodb/mongodbatlas-cloudformation-resources/blob/master/cfn-resources/stream-workspace/README.md", + "tagging": { + "taggable": false + } +} diff --git a/cfn-resources/stream-workspace/resource-role.yaml b/cfn-resources/stream-workspace/resource-role.yaml new file mode 100644 index 000000000..d2b8e433c --- /dev/null +++ b/cfn-resources/stream-workspace/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-StreamWorkspace/* + 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/stream-workspace/template.yml b/cfn-resources/stream-workspace/template.yml new file mode 100644 index 000000000..4aa052513 --- /dev/null +++ b/cfn-resources/stream-workspace/template.yml @@ -0,0 +1,28 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the MongoDB::Atlas::StreamWorkspace resource type + +Globals: + Function: + Timeout: 180 # docker start-up times can be long for SAM CLI + MemorySize: 256 + +Resources: + TypeFunction: + Type: AWS::Serverless::Function + Properties: + Handler: bootstrap + Runtime: provided.al2 + CodeUri: bin/ + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: bootstrap + Runtime: provided.al2 + CodeUri: bin/ + Environment: + Variables: + MODE: Test + LOG_LEVEL: debug + MONGODB_ATLAS_BASE_URL: https://cloud-dev.mongodb.com/ diff --git a/cfn-resources/stream-workspace/test/README.md b/cfn-resources/stream-workspace/test/README.md new file mode 100644 index 000000000..4b7e20856 --- /dev/null +++ b/cfn-resources/stream-workspace/test/README.md @@ -0,0 +1,40 @@ +# Stream Workspace + +## Prerequisites +### Resources needed to run the manual QA +- Atlas organization +- Atlas project + + +All resources are created as part of `cfn-testing-helper.sh` + +## Manual QA +Please, follows the steps in [TESTING.md](../../../TESTING.md). + + +### Success criteria when testing the resource +- The Stream workspace should be visible in the stream processing page + + + +## Important Links +- [API Documentation](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/group/endpoint-streams) +- [Resource Usage Documentation](https://www.mongodb.com/docs/atlas/stream-processing/) + +## Contract Testing + + +### Build Handler +```bash +make build +``` +### Run the handler in a docker container +```bash +# Required the docker daemon running +sam local start-lambda --skip-pull-image +``` + +### Run contract tests +```bash +cfn test --function-name TestEntrypoint --verbose +``` diff --git a/cfn-resources/stream-workspace/test/cfn-test-create-inputs.sh b/cfn-resources/stream-workspace/test/cfn-test-create-inputs.sh new file mode 100755 index 000000000..41c0ab6c1 --- /dev/null +++ b/cfn-resources/stream-workspace/test/cfn-test-create-inputs.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# cfn-test-create-inputs.sh +# +# This tool generates json files in the inputs/ for `cfn test`. +# + +set -euo pipefail + +rm -rf inputs +mkdir inputs + +projectName="${1:-${PROJECT_NAME:-test-stream-workspace}}" + +#set profile +profile="default" +if [ ${MONGODB_ATLAS_PROFILE+x} ]; then + echo "profile set to ${MONGODB_ATLAS_PROFILE}" + profile=${MONGODB_ATLAS_PROFILE} +fi + +# Try to find existing project first, or use provided project ID +if [ ${MONGODB_ATLAS_PROJECT_ID+x} ] && [ -n "${MONGODB_ATLAS_PROJECT_ID}" ]; then + projectId="${MONGODB_ATLAS_PROJECT_ID}" + echo -e "Using provided project ID: ${projectId}\n" +else + projectId=$(atlas projects list --output json | jq --arg NAME "${projectName}" -r '.results[] | select(.name==$NAME) | .id') + if [ -z "$projectId" ]; then + # Try to create project, but if IP is not on access list, use default project ID + if projectId=$(atlas projects create "${projectName}" --output=json 2>/dev/null | jq -r '.id'); then + echo -e "Created project \"${projectName}\" with id: ${projectId}\n" + else + # Fallback to project ID from environment variable + if [ -z "${MONGODB_ATLAS_PROJECT_ID:-}" ]; then + echo -e "ERROR: Could not create project and MONGODB_ATLAS_PROJECT_ID is not set. Please set MONGODB_ATLAS_PROJECT_ID environment variable.\n" + exit 1 + fi + projectId="${MONGODB_ATLAS_PROJECT_ID}" + echo -e "Could not create project (IP may not be on access list). Using project ID from environment: ${projectId}\n" + fi + else + echo -e "FOUND project \"${projectName}\" with id: ${projectId}\n" + fi +fi + +streamWorkspaceName="stream-workspace-$(date +%s)-$RANDOM" +cloudProvider="AWS" +region="VIRGINIA_USA" +tier="SP30" + +WORDTOREMOVE="template." + +cd "$(dirname "$0")" || exit +for inputFile in inputs_*; do + outputFile=${inputFile//$WORDTOREMOVE/} + jq --arg project_id "$projectId" \ + --arg stream_workspace_name "$streamWorkspaceName" \ + --arg cloud_provider "$cloudProvider" \ + --arg region "$region" \ + --arg profile "$profile" \ + --arg tier "$tier" \ + '.Profile?|=$profile | .ProjectId?|=$project_id | .WorkspaceName?|=$stream_workspace_name | .DataProcessRegion.CloudProvider?|=$cloud_provider | .DataProcessRegion.Region?|=$region | .StreamConfig.Tier?|=$tier' \ + "$inputFile" >"../inputs/$outputFile" +done + +cd .. + +ls -l inputs diff --git a/cfn-resources/stream-workspace/test/cfn-test-delete-inputs.sh b/cfn-resources/stream-workspace/test/cfn-test-delete-inputs.sh new file mode 100755 index 000000000..a19f0f89b --- /dev/null +++ b/cfn-resources/stream-workspace/test/cfn-test-delete-inputs.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# cfn-test-delete-inputs.sh +# +# This tool deletes the mongodb resources used for `cfn test` as inputs. + +set -euo pipefail + +function usage { + echo "usage:$0 " +} + +projectId=$(jq -r '.ProjectId' ./inputs/inputs_1_create.json) +workspaceName=$(jq -r '.WorkspaceName' ./inputs/inputs_1_create.json) + +# delete stream workspace (using instances delete for backward compatibility) +if atlas streams instances delete "${workspaceName}" --projectId "${projectId}" --force; then + echo "deleting stream workspace with name ${workspaceName}" +else + echo "failed to delete the stream workspace with name ${workspaceName}" +fi diff --git a/cfn-resources/stream-workspace/test/contract-testing/cfn-test-create-inputs.sh b/cfn-resources/stream-workspace/test/contract-testing/cfn-test-create-inputs.sh new file mode 100755 index 000000000..d3b3d081c --- /dev/null +++ b/cfn-resources/stream-workspace/test/contract-testing/cfn-test-create-inputs.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# Run this script with the Makefile +# make create-test-resources +# +# This tool generates json files in the inputs/ for `cfn test`. +# +set -o errexit +set -o nounset +set -o pipefail +set -x + +if [ -z "${AWS_DEFAULT_REGION+x}" ]; then + echo "AWS_DEFAULT_REGION must be set" + exit 1 +fi + +# setting projectName +projectName="stream-workspace-$(date +%s)-$RANDOM" + +./test/cfn-test-create-inputs.sh "$projectName" diff --git a/cfn-resources/stream-workspace/test/inputs_1_create.template.json b/cfn-resources/stream-workspace/test/inputs_1_create.template.json new file mode 100644 index 000000000..40beb6ac4 --- /dev/null +++ b/cfn-resources/stream-workspace/test/inputs_1_create.template.json @@ -0,0 +1,13 @@ +{ + "WorkspaceName": "", + "ProjectId": "", + "DataProcessRegion": { + "CloudProvider": "AWS", + "Region": "VIRGINIA_USA" + }, + "StreamConfig": { + "Tier": "SP30", + "MaxTierSize": "SP50" + }, + "Profile": "default" +} diff --git a/cfn-resources/stream-workspace/test/inputs_1_update.template.json b/cfn-resources/stream-workspace/test/inputs_1_update.template.json new file mode 100644 index 000000000..9325f53a7 --- /dev/null +++ b/cfn-resources/stream-workspace/test/inputs_1_update.template.json @@ -0,0 +1,13 @@ +{ + "WorkspaceName": "", + "ProjectId": "", + "DataProcessRegion": { + "CloudProvider": "AWS", + "Region": "DUBLIN_IRL" + }, + "StreamConfig": { + "Tier": "SP30", + "MaxTierSize": "SP50" + }, + "Profile": "default" +} diff --git a/cfn-resources/stream-workspace/test/stream-workspace.sample-cfn-request.json b/cfn-resources/stream-workspace/test/stream-workspace.sample-cfn-request.json new file mode 100644 index 000000000..9e269ec6e --- /dev/null +++ b/cfn-resources/stream-workspace/test/stream-workspace.sample-cfn-request.json @@ -0,0 +1,16 @@ +{ + "desiredResourceState": { + "WorkspaceName": "stream-workspace", + "ProjectId": "", + "DataProcessRegion": { + "CloudProvider": "AWS", + "Region": "VIRGINIA_USA" + }, + "StreamConfig": { + "Tier": "SP30", + "MaxTierSize": "SP50" + }, + "Profile": "default" + }, + "previousResourceState": {} +} diff --git a/cfn-resources/util/http_status.go b/cfn-resources/util/http_status.go new file mode 100644 index 000000000..588d8eb1a --- /dev/null +++ b/cfn-resources/util/http_status.go @@ -0,0 +1,33 @@ +// 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 util + +import "net/http" + +func StatusNotFound(resp *http.Response) bool { + return resp != nil && resp.StatusCode == http.StatusNotFound +} + +func StatusConflict(resp *http.Response) bool { + return resp != nil && resp.StatusCode == http.StatusConflict +} + +func StatusBadRequest(resp *http.Response) bool { + return resp != nil && resp.StatusCode == http.StatusBadRequest +} + +func StatusServiceUnavailable(resp *http.Response) bool { + return resp != nil && resp.StatusCode == http.StatusServiceUnavailable +} diff --git a/examples/atlas-streams/stream-workspace/README.md b/examples/atlas-streams/stream-workspace/README.md new file mode 100644 index 000000000..dd59690e7 --- /dev/null +++ b/examples/atlas-streams/stream-workspace/README.md @@ -0,0 +1,22 @@ +# How to create a MongoDB::Atlas::StreamWorkspace + +## Step 1: Activate the stream workspace 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::StreamWorkspace 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 StreamWorkspace Resource is ready to use. + +## Step 2: Create template using [stream-workspace.json](stream-workspace.json) + Note: Make sure you are providing appropriate values for: + 1. ProjectId + 2. WorkspaceName (optional) + 3. CloudProvider: AWS (optional, default: AWS) + 4. Region (optional, default: VIRGINIA_USA) + 5. Tier: SP2, SP5, SP10, SP30, or SP50 (optional, default: SP30) + 6. MaxTierSize (optional, default: SP50) + 7. Profile (optional) diff --git a/examples/atlas-streams/stream-workspace/stream-workspace.json b/examples/atlas-streams/stream-workspace/stream-workspace.json new file mode 100644 index 000000000..dce36d9ff --- /dev/null +++ b/examples/atlas-streams/stream-workspace/stream-workspace.json @@ -0,0 +1,151 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "MongoDB Atlas AWS CloudFormation Quickstart for MongoDB::Atlas::StreamWorkspace", + "Parameters": { + "ProjectId": { + "Description": "Your MongoDB Cloud Project ID", + "Type": "String" + }, + "WorkspaceName": { + "Description": "Name for the Stream Workspace", + "Type": "String", + "Default": "" + }, + "CloudProvider": { + "Description": "Cloud provider for data processing region (AWS only for CloudFormation)", + "Type": "String", + "Default": "AWS", + "AllowedValues": ["AWS"] + }, + "Region": { + "Description": "Region for data processing", + "Type": "String", + "Default": "VIRGINIA_USA" + }, + "Tier": { + "Description": "Stream Workspace Tier (SP2, SP5, SP10, SP30, SP50)", + "Type": "String", + "Default": "SP30", + "AllowedValues": ["SP2", "SP5", "SP10", "SP30", "SP50"] + }, + "MaxTierSize": { + "Description": "Max tier size for the Stream Workspace (optional)", + "Type": "String", + "Default": "SP50", + "AllowedValues": ["SP2", "SP5", "SP10", "SP30", "SP50"] + }, + "Profile": { + "Description": "AWS Secrets Manager profile name", + "Type": "String", + "Default": "default" + } + }, + "Conditions": { + "HasWorkspaceName": { + "Fn::Not": [ + { + "Fn::Equals": [ + { + "Ref": "WorkspaceName" + }, + "" + ] + } + ] + } + }, + "Resources": { + "StreamWorkspace": { + "Type": "MongoDB::Atlas::StreamWorkspace", + "Properties": { + "WorkspaceName": { + "Fn::If": [ + "HasWorkspaceName", + { + "Ref": "WorkspaceName" + }, + { + "Fn::Sub": "stream-workspace-${AWS::StackName}" + } + ] + }, + "ProjectId": { + "Ref": "ProjectId" + }, + "DataProcessRegion": { + "CloudProvider": { + "Ref": "CloudProvider" + }, + "Region": { + "Ref": "Region" + } + }, + "StreamConfig": { + "Tier": { + "Ref": "Tier" + }, + "MaxTierSize": { + "Ref": "MaxTierSize" + } + }, + "Profile": { + "Ref": "Profile" + } + } + } + }, + "Outputs": { + "StreamWorkspaceId": { + "Description": "The unique identifier for the Stream Workspace", + "Value": { + "Fn::GetAtt": [ + "StreamWorkspace", + "Id" + ] + }, + "Export": { + "Name": { + "Fn::Sub": "${AWS::StackName}-StreamWorkspaceId" + } + } + }, + "StreamWorkspaceName": { + "Description": "The name of the Stream Workspace", + "Value": { + "Fn::If": [ + "HasWorkspaceName", + { + "Ref": "WorkspaceName" + }, + { + "Fn::Sub": "stream-workspace-${AWS::StackName}" + } + ] + }, + "Export": { + "Name": { + "Fn::Sub": "${AWS::StackName}-StreamWorkspaceName" + } + } + }, + "StreamWorkspaceHostnames": { + "Description": "List of hostnames assigned to the stream workspace", + "Value": { + "Fn::Join": [ + ",", + { + "Fn::GetAtt": [ + "StreamWorkspace", + "Hostnames" + ] + } + ] + }, + "Export": { + "Name": { + "Fn::Sub": "${AWS::StackName}-StreamWorkspaceHostnames" + } + } + } + } +}