diff --git a/.changelog/3700.txt b/.changelog/3700.txt new file mode 100644 index 0000000000..58ab43ce66 --- /dev/null +++ b/.changelog/3700.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +provider: Supports Service Account as credentials to authenticate the provider +``` diff --git a/.changelog/3716.txt b/.changelog/3716.txt new file mode 100644 index 0000000000..f45fa9226e --- /dev/null +++ b/.changelog/3716.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +provider: Supports Service Account JWT Token as credentials to authenticate the provider +``` diff --git a/.changelog/3738.txt b/.changelog/3738.txt new file mode 100644 index 0000000000..2c423e83a5 --- /dev/null +++ b/.changelog/3738.txt @@ -0,0 +1,3 @@ +```release-note:bug +provider: Enforces strict hierarchy when selecting the credential source such as AWS Secrets Manager, provider attributes, or environment variables to prevent combining with values from different sources +``` diff --git a/.github/workflows/acceptance-tests-runner.yml b/.github/workflows/acceptance-tests-runner.yml index 18f02986ca..a119c30c24 100644 --- a/.github/workflows/acceptance-tests-runner.yml +++ b/.github/workflows/acceptance-tests-runner.yml @@ -163,6 +163,10 @@ on: required: true mongodb_atlas_rp_public_key: required: true + mongodb_atlas_rp_client_id: + required: true + mongodb_atlas_rp_client_secret: + required: true mongodb_atlas_client_id: required: true mongodb_atlas_client_secret: @@ -206,9 +210,10 @@ env: TF_ACC: 1 TF_LOG: ${{ vars.LOG_LEVEL }} ACCTEST_TIMEOUT: ${{ vars.ACCTEST_TIMEOUT }} - # Only Migration tests are run when a specific previous provider version is set - # If the name (regex) of the test is set, only that test is run - ACCTEST_REGEX_RUN: ${{ inputs.test_name || inputs.provider_version == '' && '^Test(Acc|Mig)' || '^TestMig' }} + # If the name (regex) of the test is set, only that test is run. + # Don't run migration tests if using Service Accounts because previous provider versions don't support SA yet. + # Only Migration tests are run when a specific previous provider version is set. + ACCTEST_REGEX_RUN: ${{ inputs.test_name || inputs.use_sa && '^TestAcc' || inputs.provider_version == '' && '^Test(Acc|Mig)' || '^TestMig' }} MONGODB_ATLAS_BASE_URL: ${{ inputs.mongodb_atlas_base_url }} MONGODB_REALM_BASE_URL: ${{ inputs.mongodb_realm_base_url }} MONGODB_ATLAS_ORG_ID: ${{ inputs.mongodb_atlas_org_id }} @@ -554,8 +559,39 @@ jobs: MONGODB_ATLAS_CLIENT_ID: ${{ secrets.mongodb_atlas_client_id }} MONGODB_ATLAS_CLIENT_SECRET: ${{ secrets.mongodb_atlas_client_secret }} MONGODB_ATLAS_LAST_VERSION: ${{ needs.get-provider-version.outputs.provider_version }} - ACCTEST_REGEX_RUN: '^TestUnexisting' # TODO: SA not implemented in master yet - # ACCTEST_REGEX_RUN: '^TestAccServiceAccount' + ACCTEST_REGEX_RUN: '^TestAccServiceAccount' + ACCTEST_PACKAGES: ./internal/provider + run: make testacc + - name: Generate OAuth2 Token + id: generate-token + shell: bash + env: + MONGODB_ATLAS_BASE_URL: ${{ inputs.mongodb_atlas_base_url }} + MONGODB_ATLAS_CLIENT_ID: ${{ secrets.mongodb_atlas_client_id }} + MONGODB_ATLAS_CLIENT_SECRET: ${{ secrets.mongodb_atlas_client_secret }} + run: | + if ! ACCESS_TOKEN=$(make generate-oauth2-token); then + echo "Error: Failed to generate access token" + exit 1 + fi + if [ -z "$ACCESS_TOKEN" ]; then + echo "Error: Generated access token is empty" + exit 1 + fi + { + echo "access_token<> "$GITHUB_OUTPUT" + - name: Acceptance Tests (Access Token) + env: + MONGODB_ATLAS_PUBLIC_KEY: "" + MONGODB_ATLAS_PRIVATE_KEY: "" + MONGODB_ATLAS_CLIENT_ID: "" + MONGODB_ATLAS_CLIENT_SECRET: "" + MONGODB_ATLAS_ACCESS_TOKEN: ${{ steps.generate-token.outputs.access_token }} + MONGODB_ATLAS_LAST_VERSION: ${{ needs.get-provider-version.outputs.provider_version }} + ACCTEST_REGEX_RUN: '^TestAccAccessToken' ACCTEST_PACKAGES: ./internal/provider run: make testacc - name: Acceptance Tests (Service Account smoke tests) # small selection of fast tests to run with SA @@ -565,7 +601,7 @@ jobs: MONGODB_ATLAS_CLIENT_ID: ${{ secrets.mongodb_atlas_client_id }} MONGODB_ATLAS_CLIENT_SECRET: ${{ secrets.mongodb_atlas_client_secret }} MONGODB_ATLAS_LAST_VERSION: ${{ needs.get-provider-version.outputs.provider_version }} - ACCTEST_REGEX_RUN: '^TestUnexisting' # TODO: SA not implemented in master yet + ACCTEST_REGEX_RUN: '^TestAcc' # Don't run migration tests because previous provider versions don't support SA. ACCTEST_PACKAGES: | ./internal/service/alertconfiguration ./internal/service/databaseuser @@ -1176,12 +1212,13 @@ jobs: terraform_wrapper: false - name: Acceptance Tests env: + MONGODB_ATLAS_PUBLIC_KEY: ${{ inputs.use_sa == false && secrets.mongodb_atlas_rp_public_key || '' }} + MONGODB_ATLAS_PRIVATE_KEY: ${{ inputs.use_sa == false && secrets.mongodb_atlas_rp_private_key || '' }} + MONGODB_ATLAS_CLIENT_ID: ${{ inputs.use_sa && secrets.mongodb_atlas_rp_client_id || '' }} + MONGODB_ATLAS_CLIENT_SECRET: ${{ inputs.use_sa && secrets.mongodb_atlas_rp_client_secret || '' }} MONGODB_ATLAS_ORG_ID: ${{ inputs.mongodb_atlas_rp_org_id }} - MONGODB_ATLAS_PUBLIC_KEY: ${{ secrets.mongodb_atlas_rp_public_key }} - MONGODB_ATLAS_PRIVATE_KEY: ${{ secrets.mongodb_atlas_rp_private_key }} MONGODB_ATLAS_LAST_VERSION: ${{ needs.get-provider-version.outputs.provider_version }} - ACCTEST_PACKAGES: | - ./internal/service/resourcepolicy + ACCTEST_PACKAGES: ./internal/service/resourcepolicy run: make testacc search_deployment: diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 2d07558e29..54eb19e6e7 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -82,6 +82,8 @@ jobs: mongodb_atlas_gov_private_key: ${{ inputs.atlas_cloud_env == 'qa' && secrets.MONGODB_ATLAS_GOV_PRIVATE_KEY_QA || secrets.MONGODB_ATLAS_GOV_PRIVATE_KEY_DEV }} mongodb_atlas_rp_public_key: ${{ inputs.atlas_cloud_env == 'qa' && secrets.MONGODB_ATLAS_RP_PUBLIC_KEY_QA || secrets.MONGODB_ATLAS_RP_PUBLIC_KEY_DEV }} mongodb_atlas_rp_private_key: ${{ inputs.atlas_cloud_env == 'qa' && secrets.MONGODB_ATLAS_RP_PRIVATE_KEY_QA || secrets.MONGODB_ATLAS_RP_PRIVATE_KEY_DEV }} + mongodb_atlas_rp_client_id: ${{ inputs.atlas_cloud_env == 'qa' && secrets.MONGODB_ATLAS_RP_CLIENT_ID_QA || secrets.MONGODB_ATLAS_RP_CLIENT_ID_DEV }} + mongodb_atlas_rp_client_secret: ${{ inputs.atlas_cloud_env == 'qa' && secrets.MONGODB_ATLAS_RP_CLIENT_SECRET_QA || secrets.MONGODB_ATLAS_RP_CLIENT_SECRET_DEV }} mongodb_atlas_client_id: ${{ inputs.atlas_cloud_env == 'qa' && secrets.MONGODB_ATLAS_CLIENT_ID_QA || secrets.MONGODB_ATLAS_CLIENT_ID_DEV }} mongodb_atlas_client_secret: ${{ inputs.atlas_cloud_env == 'qa' && secrets.MONGODB_ATLAS_CLIENT_SECRET_QA || secrets.MONGODB_ATLAS_CLIENT_SECRET_DEV }} ca_cert: ${{ secrets.CA_CERT }} diff --git a/Makefile b/Makefile index a475f38f88..7c4517c104 100644 --- a/Makefile +++ b/Makefile @@ -43,11 +43,16 @@ test: fmtcheck ## Run unit tests @$(eval export MONGODB_ATLAS_ORG_ID?=111111111111111111111111) @$(eval export MONGODB_ATLAS_PROJECT_ID?=111111111111111111111111) @$(eval export MONGODB_ATLAS_CLUSTER_NAME?=mocked-cluster) + @$(eval export MONGODB_ATLAS_PUBLIC_KEY=) + @$(eval export MONGODB_ATLAS_PRIVATE_KEY=) + @$(eval export MONGODB_ATLAS_CLIENT_ID=) + @$(eval export MONGODB_ATLAS_CLIENT_SECRET=) + @$(eval export MONGODB_ATLAS_ACCESS_TOKEN=) go test ./... -timeout=120s -parallel=$(PARALLEL_GO_TEST) -race .PHONY: testmact testmact: ## Run MacT tests (mocked acc tests) - @$(eval ACCTEST_REGEX_RUN?=^TestAccMockable) + @$(eval export ACCTEST_REGEX_RUN?=^TestAccMockable) @$(eval export HTTP_MOCKER_REPLAY?=true) @$(eval export HTTP_MOCKER_CAPTURE?=false) @$(eval export MONGODB_ATLAS_ORG_ID?=111111111111111111111111) @@ -72,7 +77,7 @@ testmact-capture: ## Capture HTTP traffic for MacT tests .PHONY: testacc testacc: fmtcheck ## Run acc & mig tests (acceptance & migration tests) - @$(eval ACCTEST_REGEX_RUN?=^TestAcc) + @$(eval export ACCTEST_REGEX_RUN?=^TestAcc) TF_ACC=1 go test $(ACCTEST_PACKAGES) -run '$(ACCTEST_REGEX_RUN)' -v -parallel $(PARALLEL_GO_TEST) $(TESTARGS) -timeout $(ACCTEST_TIMEOUT) -ldflags="$(LINKER_FLAGS)" .PHONY: testaccgov @@ -196,6 +201,10 @@ check-changelog-entry-file: ## Check a changelog entry file in a PR jira-release-version: ## Update Jira version in a release go run ./tools/jira-release-version/*.go +.PHONY: generate-oauth2-token +generate-oauth2-token: ## Generate OAuth2 access token from Service Account credentials + @go run ./tools/generate-oauth2-token/*.go + .PHONY: enable-autogen enable-autogen: ## Enable use of autogen resources in the provider $(eval filename := ./internal/provider/provider.go) diff --git a/docs/guides/provider-configuration.md b/docs/guides/provider-configuration.md new file mode 100644 index 0000000000..77eeaeafcd --- /dev/null +++ b/docs/guides/provider-configuration.md @@ -0,0 +1,176 @@ +--- +page_title: "Guide: Provider Configuration" +--- + +# Provider Configuration + +This guide covers authentication and configuration options for the MongoDB Atlas Provider. + +## Authentication Methods + +The MongoDB Atlas provider supports the following authentication methods: + +1. [**Service Account (SA)** - Recommended](#service-account-recommended) +2. [**Programmatic Access Key (PAK)**](#programmatic-access-key) + +Credentials can be provided through (in priority order): + +- AWS Secrets Manager +- Provider attributes +- Environment variables + +The provider uses the first available credentials source. + +### Service Account (Recommended) + +SAs simplify authentication by eliminating the need to create new Atlas-specific user identities and permission credentials. See [Service Accounts Overview](https://www.mongodb.com/docs/atlas/api/service-accounts-overview/) and [MongoDB Atlas Service Account Limits](https://www.mongodb.com/docs/manual/reference/limits/#mongodb-atlas-service-account-limits) for more information. + +To use SA authentication, create an SA in your [MongoDB Atlas organization](https://www.mongodb.com/docs/atlas/configure-api-access/#grant-programmatic-access-to-an-organization) and set the credentials, for example: + +```terraform +provider "mongodbatlas" { + client_id = var.mongodbatlas_client_id + client_secret = var.mongodbatlas_client_secret +} +``` + +**Note:** SAs can't be used with `mongodbatlas_event_trigger` resources because its API doesn't support it yet. + +### Programmatic Access Key + +Generate a PAK with the appropriate [role](https://docs.atlas.mongodb.com/reference/user-roles/). See the [MongoDB Atlas documentation](https://www.mongodb.com/docs/atlas/configure-api-access-org/) for detailed instructions. + +**Role recommendation:** If unsure which role to grant, use an organization API key with the Organization Owner role to ensure sufficient access as in the following example: + +```terraform +provider "mongodbatlas" { + public_key = var.mongodbatlas_public_key + private_key = var.mongodbatlas_private_key +} +``` + +~> **Migrating from PAK to SA:** Update your provider attributes or environment variables to use SA credentials instead of PAK credentials, then run `terraform plan` to verify everything works correctly. + +## AWS Secrets Manager + +The provider supports retrieving credentials from AWS Secrets Manager. See [AWS Secrets Manager documentation](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html) for more details. + +### Setup Instructions + +1. **Create secrets in AWS Secrets Manager** + + For SA, create a secret with the following key-value pairs: + - `client_id`: your-client-id + - `client_secret`: your-client-secret + + For PAK, create a secret with the following key-value pairs: + - `public_key`: your-public-key + - `private_key`: your-private-key + +2. **Create an IAM Role** with: + - Permission for `sts:AssumeRole` + - Attached AWS managed policy `SecretsManagerReadWrite` + +3. **Configure AWS credentials** (using AWS CLI or environment variables) + +4. **Assume the role** to obtain STS credentials + + ```shell + aws sts assume-role --role-arn --role-session-name newSession + ``` + +5. **Configure provider with AWS Secrets Manager** + + Using provider attributes: + + ```terraform + provider "mongodbatlas" { + aws_access_key_id = var.aws_access_key_id + aws_secret_access_key = var.aws_secret_access_key + aws_session_token = var.aws_session_token + assume_role = "arn:aws:iam:::role/mdbsts" + secret_name = "mongodbsecret" + region = "us-east-2" + } + ``` + + Alternatively, you can use environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `ASSUME_ROLE_ARN`, `SECRET_NAME`, `AWS_REGION`). + +### Cross-Account and Cross-Region Access + +For cross-account secrets, use the fully qualified ARN for `secret_name`. For cross-region or cross-account access, the `sts_endpoint` parameter is required, for example: + +```terraform +provider "mongodbatlas" { + aws_access_key_id = var.aws_access_key_id + aws_secret_access_key = var.aws_secret_access_key + aws_session_token = var.aws_session_token + assume_role = "arn:aws:iam:::role/mdbsts" + secret_name = "arn:aws:secretsmanager:us-east-1::secret:test789-TO06Hy" + region = "us-east-2" + sts_endpoint = "https://sts.us-east-2.amazonaws.com/" +} +``` + +## Provider Configuration Reference + +### Provider Arguments + +* `client_id` - (Optional) SA Client ID (env: `MONGODB_ATLAS_CLIENT_ID`). +* `client_secret` - (Optional) SA Client Secret (env: `MONGODB_ATLAS_CLIENT_SECRET`). +* `access_token` - (Optional) SA Access Token (env: `MONGODB_ATLAS_ACCESS_TOKEN`). Instead of using Client ID and Client Secret, you can generate and use an SA token directly. See [Generate Service Account Token](https://www.mongodb.com/docs/atlas/api/service-accounts/generate-oauth2-token/#std-label-generate-oauth2-token-atlas) for details. Note: tokens have expiration times. +* `public_key` - (Optional) PAK Public Key (env: `MONGODB_ATLAS_PUBLIC_API_KEY`). +* `private_key` - (Optional) PAK Private Key (env: `MONGODB_ATLAS_PRIVATE_API_KEY`). +* `base_url` - (Optional) MongoDB Atlas Base URL (env: `MONGODB_ATLAS_BASE_URL`). For advanced use cases, you can configure custom API endpoints. +* `realm_base_url` - (Optional) MongoDB Realm Base URL (env: `MONGODB_REALM_BASE_URL`). +* `is_mongodbgov_cloud` - (Optional) Set to `true` to use MongoDB Atlas for Government, a dedicated deployment option for government agencies and contractors requiring FedRAMP compliance. When enabled, the provider uses government-specific API endpoints. Ensure credentials are created in the government environment. See [Atlas for Government Considerations](https://www.mongodb.com/docs/atlas/government/api/#atlas-for-government-considerations) for feature limitations and requirements. + ```terraform + provider "mongodbatlas" { + client_id = var.mongodbatlas_client_id + client_secret = var.mongodbatlas_client_secret + is_mongodbgov_cloud = true + } + ``` +* `assume_role` - (Optional) AWS IAM role configuration for accessing secrets in AWS Secrets Manager. Role ARN env: `ASSUME_ROLE_ARN`. See [AWS Secrets Manager](#aws-secrets-manager) section for details. +* `secret_name` - (Optional) Name of the secret in AWS Secrets Manager (env: `SECRET_NAME`). +* `region` - (Optional) AWS region where the secret is stored (env: `AWS_REGION`). +* `aws_access_key_id` - (Optional) AWS Access Key ID (env: `AWS_ACCESS_KEY_ID`). +* `aws_secret_access_key` - (Optional) AWS Secret Access Key (env: `AWS_SECRET_ACCESS_KEY`). +* `aws_session_token` - (Optional) AWS Session Token (env: `AWS_SESSION_TOKEN`). +* `sts_endpoint` - (Optional) AWS STS endpoint (env: `STS_ENDPOINT`). + +## Credential Priority + +When multiple credentials are provided in the same source, the provider uses this priority order: + +1. Access Token +2. Service Account (SA) +3. Programmatic Access Key (PAK) + +The provider displays a warning when multiple credentials are detected. + +## Supported OS and Architectures + +As per [HashiCorp's recommendations](https://developer.hashicorp.com/terraform/registry/providers/os-arch), the MongoDB Atlas Provider fully supports the following operating system / architecture combinations: + +- Darwin / AMD64 +- Darwin / ARMv8 +- Linux / AMD64 +- Linux / ARMv8 (AArch64/ARM64) +- Linux / ARMv6 +- Windows / AMD64 + +We ship binaries but do not prioritize fixes for the following operating system / architecture combinations: + +- Linux / 386 +- Windows / 386 +- FreeBSD / 386 +- FreeBSD / AMD64 + +## Additional Resources + +- [MongoDB Atlas API Documentation](https://www.mongodb.com/docs/atlas/api/) +- [Service Accounts Overview](https://www.mongodb.com/docs/atlas/api/service-accounts-overview/) +- [Configure API Access](https://www.mongodb.com/docs/atlas/configure-api-access/) +- [Atlas for Government](https://www.mongodb.com/docs/atlas/government/) +- [Terraform Provider Documentation](https://registry.terraform.io/providers/mongodb/mongodbatlas/latest/docs) diff --git a/docs/index.md b/docs/index.md index adeb233c8d..0496159637 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,180 +1,83 @@ # MongoDB Atlas Provider -You can use the MongoDB Atlas provider to interact with the resources supported by [MongoDB Atlas](https://www.mongodb.com/cloud/atlas). -The provider needs to be configured with the proper credentials before it can be used. - -Use the navigation to the left to read about the available provider resources and data sources. - -See [CHANGELOG](https://github.com/mongodb/terraform-provider-mongodbatlas/blob/master/CHANGELOG.md) for current version information. +The MongoDB Atlas provider is used to interact with the resources supported by [MongoDB Atlas](https://www.mongodb.com/cloud/atlas). The provider needs to be configured with proper credentials before it can be used. ## Example Usage -```terraform -# Configure the MongoDB Atlas Provider -provider "mongodbatlas" { - public_key = var.mongodbatlas_public_key - private_key = var.mongodbatlas_private_key -} -# Create the resources -``` - -### Provider and terraform version constraints - -We recommend that you pin your Atlas [provider version](https://developer.hashicorp.com/terraform/language/providers/requirements#version) to at least the [major version](#versioning-strategy) (e.g. `~> 2.0`) to avoid accidental upgrades to incompatible new versions. Starting on `2.0.0`, the [MongoDB Atlas Provider Versioning Policy](#mongodb-atlas-provider-versioning-policy) ensures that minor and patch versions do not include [Breaking Changes](#definition-of-breaking-changes). - -For Terraform version, we recommend that you use the latest [HashiCorp Terraform Core Version](https://github.com/hashicorp/terraform). For more details see [HashiCorp Terraform Version Compatibility Matrix](#hashicorp-terraform-version-compatibility-matrix). - -## Configure Atlas Programmatic Access - -In order to set up authentication with the MongoDB Atlas provider, you must generate a programmatic API key for MongoDB Atlas with the appropriate [role](https://docs.atlas.mongodb.com/reference/user-roles/). -The [MongoDB Atlas documentation](https://docs.atlas.mongodb.com/tutorial/manage-programmatic-access/index.html) contains the most up-to-date instructions for creating and managing your key(s), setting the appropriate role, and optionally configuring IP access. +This example shows how to set up the MongoDB Atlas provider and create a cluster: -**Role**: If unsure of which role level to grant your key, we suggest creating an organization API Key with an Organization Owner role. This ensures that you have sufficient access for all actions. - -## Configure MongoDB Atlas for Government - -In order to enable the Terraform MongoDB Atlas Provider for use with MongoDB Atlas for Government add is_mongodbgov_cloud = true to your provider configuration: ```terraform -# Configure the MongoDB Atlas Provider for MongoDB Atlas for Government provider "mongodbatlas" { - public_key = var.mongodbatlas_public_key - private_key = var.mongodbatlas_private_key - is_mongodbgov_cloud = true + client_id = var.mongodbatlas_client_id + client_secret = var.mongodbatlas_client_secret } -# Create the resources -``` -Also see [`Atlas for Government Considerations`](https://www.mongodb.com/docs/atlas/government/api/#atlas-for-government-considerations). - -## Authenticate the Provider - -The MongoDB Atlas provider offers a flexible means of providing credentials for authentication. -You can use any the following methods: - -### Environment Variables - -You can also provide your credentials via the environment variables, -`MONGODB_ATLAS_PUBLIC_API_KEY` and `MONGODB_ATLAS_PRIVATE_API_KEY`, -for your public and private MongoDB Atlas programmatic API key pair respectively: - -```terraform -provider "mongodbatlas" {} -``` - -Usage (prefix the export commands with a space to avoid the keys being recorded in OS history): -```shell -$ export MONGODB_ATLAS_PUBLIC_API_KEY="" -$ export MONGODB_ATLAS_PRIVATE_API_KEY="" -$ terraform plan -``` - -We recommend that you use the `MONGODB_ATLAS_PUBLIC_API_KEY` and `MONGODB_ATLAS_PRIVATE_API_KEY` environment variables because they are compatible with other MongoDB tools, such as Atlas CLI. -You can still use `MONGODB_ATLAS_PUBLIC_KEY` and `MONGODB_ATLAS_PRIVATE_KEY` as alternative keys in your local environment. However, these environment variables are not guaranteed to work across all tools in the MongoDB ecosystem. - -### AWS Secrets Manager -AWS Secrets Manager (AWS SM) helps to manage, retrieve, and rotate database credentials, API keys, and other secrets throughout their lifecycles. See [product page](https://aws.amazon.com/secrets-manager/) and [documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/what-is-systems-manager.html) for more details. +# Create a project +resource "mongodbatlas_project" "this" { + name = "my-project" + org_id = var.org_id +} -In order to enable the Terraform MongoDB Atlas Provider with AWS SM, please follow the below steps: +# Create a cluster +resource "mongodbatlas_advanced_cluster" "this" { + project_id = mongodbatlas_project.this.id + name = "my-cluster" + cluster_type = "REPLICASET" -1. Create Atlas API Keys and add them as one secret to AWS SM with a raw value. Take note of which AWS Region secret is being stored in. Public Key and Private Key each need to be entered as their own key value pair. See below example: -``` - { - "public_key": "secret1", - "private_key":"secret2" - } -``` -2. Create an AWS IAM Role to attach to the AWS STS (Security Token Service) generated short lived API keys. This is required since STS generated API Keys by default have restricted permissions and need to have their permissions elevated in order to authenticate with Terraform. Take note of Role ARN and ensure IAM Role has permission for “sts:AssumeRole”. For example: -``` -{ - "Version": "2012-10-17", - "Statement": [ + replication_specs = [ + { + region_configs = [ { - "Sid": "Statement1", - "Effect": "Allow", - "Principal": { - "AWS": "*" - }, - "Action": "sts:AssumeRole" + region_name = "US_EAST_1" + priority = 7 + provider_name = "AWS" + electable_specs = { + instance_size = "M10" + node_count = 3 + } } - ] + ] + } + ] } ``` -In addition, you are required to also attach the AWS Managed policy of `SecretsManagerReadWrite` to this IAM role. -Note: this policy may be overly broad for many use cases, feel free to adjust accordingly to your organization's needs. +## Authentication -3. In terminal, store as environmental variables AWS API Keys (while you can also hardcode in config files these will then be stored as plain text in .tfstate file and should be avoided if possible). For example: -``` -export AWS_ACCESS_KEY_ID='' -export AWS_SECRET_ACCESS_KEY='' -``` -4. In terminal, use the AWS CLI command: `aws sts assume-role --role-arn ROLE_ARN_FROM_ABOVE --role-session-name newSession` - -Note: AWS STS secrets are short lived by default, use the ` --duration-seconds` flag to specify longer duration as needed - -5. Store each of the 3 new created secrets from AWS STS as environment variables (hardcoding secrets into config file with additional risk is also supported). For example: -``` -export AWS_ACCESS_KEY_ID='' -export AWS_SECRET_ACCESS_KEY='' -export AWS_SESSION_TOKEN="" -``` +The MongoDB Atlas provider uses Service Account (SA) as the recommended authentication method. -6. Add assume_role block with `role_arn`, `secret_name`, and AWS `region` where secret is stored as part of AWS SM. Each of these 3 fields are REQUIRED. For example: -```terraform -# Configure the MongoDB Atlas Provider to Authenticate with AWS Secrets Manager -provider "mongodbatlas" { - assume_role { - role_arn = "arn:aws:iam:::role/mdbsts" - } - secret_name = "mongodbsecret" - // fully qualified secret_name ARN also supported as input "arn:aws:secretsmanager:af-south-1::secret:test789-TO06Hy" - region = "us-east-2" - - aws_access_key_id = "" - aws_secret_access_key = "" - aws_session_token = "" - sts_endpoint = "https://sts.us-east-2.amazonaws.com/" -} -``` -Note: `aws_access_key_id`, `aws_secret_access_key`, and `aws_session_token` can also be passed in using environment variables i.e. aws_access_key_id will accept AWS_ACCESS_KEY_ID and TF_VAR_AWS_ACCESS_KEY_ID as a default value in place of value in a terraform file variable. +For detailed authentication configuration, see: +- [Service Account (SA)](guides/provider-configuration#service-account-recommended) +- [Programmatic Access Key (PAK)](guides/provider-configuration#programmatic-access-key) +- [AWS Secrets Manager integration](guides/provider-configuration#aws-secrets-manager) -Note: Fully qualified `secret_name` ARN as input is REQUIRED for cross-AWS account secrets. For more detatils see: -* https://aws.amazon.com/blogs/security/how-to-access-secrets-across-aws-accounts-by-attaching-resource-based-policies/ -* https://aws.amazon.com/premiumsupport/knowledge-center/secrets-manager-share-between-accounts/ +## MongoDB Atlas for Government -Note: `sts_endpoint` parameter is REQUIRED for cross-AWS region or cross-AWS account secrets. +MongoDB Atlas for Government is a dedicated deployment option for government agencies and contractors requiring FedRAMP compliance. +For more details on configuration, see the [Provider Configuration Guide](guides/provider-configuration#provider-arguments). -7. In terminal, `terraform init` +## Version Requirements -### Static Credentials +### Provider Version -Static credentials can be provided by adding the following attributes in-line in the MongoDB Atlas provider block, -either directly or via input variable/local value: +We recommend pinning your provider version to at least the major version (e.g., `~> 2.0`) to avoid accidental upgrades to incompatible new versions: ```terraform -provider "mongodbatlas" { - public_key = "atlas_public_api_key" #required - private_key = "atlas_private_api_key" #required +terraform { + required_providers { + mongodbatlas = { + source = "mongodb/mongodbatlas" + version = "~> 2.0" + } + } } ``` -~> *IMPORTANT* Hard-coding your MongoDB Atlas programmatic API key pair into a Terraform configuration is not recommended. -Consider the risks, especially the inadvertent submission of a configuration file containing secrets to a public repository. - -## Argument Reference - -In addition to [generic `provider` arguments](https://www.terraform.io/docs/configuration/providers.html) -(e.g. `alias` and `version`), the MongoDB Atlas `provider` supports the following arguments: +Starting with version 2.0.0, the [MongoDB Atlas Provider Versioning Policy](#mongodb-atlas-provider-versioning-policy) ensures that minor and patch versions do not include breaking changes. -* `public_key` - (Optional) This is the public key of your MongoDB Atlas API key pair. It must be - provided, but it can also be sourced from the `MONGODB_ATLAS_PUBLIC_KEY` or `MCLI_PUBLIC_API_KEY` - environment variable. +### Terraform Version -* `private_key` - (Optional) This is the private key of your MongoDB Atlas key pair. It must be - provided, but it can also be sourced from the `MONGODB_ATLAS_PRIVATE_KEY` or `MCLI_PRIVATE_API_KEY` - environment variable. - -For more information on configuring and managing programmatic API Keys see the [MongoDB Atlas Documentation](https://docs.atlas.mongodb.com/tutorial/manage-programmatic-access/index.html). +We recommend using the latest [HashiCorp Terraform Core Version](https://github.com/hashicorp/terraform). See the [HashiCorp Terraform Version Compatibility Matrix](#hashicorp-terraform-version-compatibility-matrix) below for supported versions. ## MongoDB Atlas Provider Versioning Policy @@ -247,7 +150,7 @@ We are committed to clear and proactive communication: --- -## [HashiCorp Terraform Version](https://www.terraform.io/downloads.html) Compatibility Matrix +## HashiCorp Terraform Version Compatibility Matrix @@ -267,19 +170,8 @@ For the safety of our users, we require only consuming versions of HashiCorp Ter HashiCorp Terraform versions that are not listed on this table are no longer supported by MongoDB Atlas. For latest HashiCorp Terraform versions see [here](https://endoflife.date/terraform ). ## Supported OS and Architectures -As per [HashiCorp's recommendations](https://developer.hashicorp.com/terraform/registry/providers/os-arch), we fully support the following operating system / architecture combinations: -- Darwin / AMD64 -- Darwin / ARMv8 -- Linux / AMD64 -- Linux / ARMv8 (sometimes referred to as AArch64 or ARM64) -- Linux / ARMv6 -- Windows / AMD64 - -We ship binaries but do not prioritize fixes for the following operating system / architecture combinations: -- Linux / 386 -- Windows / 386 -- FreeBSD / 386 -- FreeBSD / AMD64 + +The MongoDB Atlas Provider supports multiple operating systems and architectures. See the [Provider Configuration Guide](guides/provider-configuration#supported-os-and-architectures) for the complete list of supported platforms. ## Helpful Links/Information diff --git a/go.mod b/go.mod index 7680ec0156..4581f946d4 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,6 @@ require ( github.com/zclconf/go-cty v1.17.0 go.mongodb.org/atlas v0.38.0 go.mongodb.org/atlas-sdk/v20240530005 v20240530005.0.0 - go.mongodb.org/atlas-sdk/v20240805005 v20240805005.0.1-0.20250402112219-2468c5354718 // uses api-bot-update-v20240805-backport-cluster to support AdvancedConfiguration in create/updateCluster APIs go.mongodb.org/atlas-sdk/v20241113005 v20241113005.0.0 go.mongodb.org/realm v0.1.0 gopkg.in/yaml.v3 v3.0.1 @@ -43,6 +42,7 @@ require ( github.com/hashicorp/terraform-json v0.27.2 github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0 go.mongodb.org/atlas-sdk/v20250312008 v20250312008.0.0 + golang.org/x/oauth2 v0.31.0 ) require ( @@ -163,7 +163,6 @@ require ( golang.org/x/crypto v0.42.0 // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect - golang.org/x/oauth2 v0.31.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect diff --git a/go.sum b/go.sum index 018850fc8a..8e130562c5 100644 --- a/go.sum +++ b/go.sum @@ -1364,8 +1364,6 @@ go.mongodb.org/atlas v0.38.0 h1:zfwymq20GqivGwxPZfypfUDry+WwMGVui97z1d8V4bU= go.mongodb.org/atlas v0.38.0/go.mod h1:DJYtM+vsEpPEMSkQzJnFHrT0sP7ev6cseZc/GGjJYG8= go.mongodb.org/atlas-sdk/v20240530005 v20240530005.0.0 h1:d/gbYJ+obR0EM/3DZf7+ZMi2QWISegm3mid7Or708cc= go.mongodb.org/atlas-sdk/v20240530005 v20240530005.0.0/go.mod h1:O47ZrMMfcWb31wznNIq2PQkkdoFoK0ea2GlmRqGJC2s= -go.mongodb.org/atlas-sdk/v20240805005 v20240805005.0.1-0.20250402112219-2468c5354718 h1:M2mNSBdTkP+paQ1qZ6FliiPdTEbDR9m9qvv4vsWoJAw= -go.mongodb.org/atlas-sdk/v20240805005 v20240805005.0.1-0.20250402112219-2468c5354718/go.mod h1:PeByRxdvzfvz7xhG5vDn60j836EoduWqTqs76okUc9c= go.mongodb.org/atlas-sdk/v20241113005 v20241113005.0.0 h1:aaU2E4rtzYXuEDxv9MoSON2gOEAA9M2gsDf2CqjcGj8= go.mongodb.org/atlas-sdk/v20241113005 v20241113005.0.0/go.mod h1:eV9REWR36iVMrpZUAMZ5qPbXEatoVfmzwT+Ue8yqU+U= go.mongodb.org/atlas-sdk/v20250312008 v20250312008.0.0 h1:Pzrb2bPXtkw1vDTiFxovZyYD4BIA4l0o6c2/HBqxe0I= diff --git a/internal/config/client.go b/internal/config/client.go index b0aad4304a..2130795887 100644 --- a/internal/config/client.go +++ b/internal/config/client.go @@ -7,12 +7,10 @@ import ( "net" "net/http" "net/url" - "strconv" "strings" "time" admin20240530 "go.mongodb.org/atlas-sdk/v20240530005/admin" - admin20240805 "go.mongodb.org/atlas-sdk/v20240805005/admin" admin20241113 "go.mongodb.org/atlas-sdk/v20241113005/admin" "go.mongodb.org/atlas-sdk/v20250312008/admin" matlasClient "go.mongodb.org/atlas/mongodbatlas" @@ -22,15 +20,15 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/mongodb-forks/digest" adminpreview "github.com/mongodb/atlas-sdk-go/admin" - "github.com/spf13/cast" "github.com/mongodb/terraform-provider-mongodbatlas/version" + + "golang.org/x/oauth2" ) const ( - toolName = "terraform-provider-mongodbatlas" - terraformPlatformName = "Terraform" - previewV2AdvancedClusterEnabledUAKey = "AdvancedClusterPreview" + toolName = "terraform-provider-mongodbatlas" + terraformPlatformName = "Terraform" timeout = 5 * time.Second keepAlive = 30 * time.Second @@ -40,6 +38,15 @@ const ( expectContinueTimeout = 1 * time.Second ) +type AuthMethod int + +const ( + Unknown AuthMethod = iota + AccessToken + ServiceAccount + Digest +) + var baseTransport = &http.Transport{ DialContext: (&net.Dialer{ Timeout: timeout, @@ -52,180 +59,171 @@ var baseTransport = &http.Transport{ ExpectContinueTimeout: expectContinueTimeout, } -// MongoDBClient contains the mongodbatlas clients and configurations -type MongoDBClient struct { - Atlas *matlasClient.Client - AtlasV2 *admin.APIClient - AtlasPreview *adminpreview.APIClient - AtlasV220240805 *admin20240805.APIClient // used in advanced_cluster to avoid adopting 2024-10-23 release with ISS autoscaling - AtlasV220240530 *admin20240530.APIClient // used in advanced_cluster and cloud_backup_schedule for avoiding breaking changes (supporting deprecated replication_specs.id) - AtlasV220241113 *admin20241113.APIClient // used in teams and atlas_users to avoiding breaking changes - Config *Config +// networkLoggingBaseTransport should be used as a base for authentication transport so authentication requests can be logged. +func networkLoggingBaseTransport() http.RoundTripper { + return NewTransportWithNetworkLogging(baseTransport, logging.IsDebugOrHigher()) } -// Config contains the configurations needed to use SDKs -type Config struct { - AssumeRoleARN string - PublicKey string - PrivateKey string - BaseURL string - RealmBaseURL string - TerraformVersion string - PreviewV2AdvancedClusterEnabled bool +// tfLoggingInterceptor should wrap the authentication transport to add Terraform logging. +func tfLoggingInterceptor(base http.RoundTripper) http.RoundTripper { + // Don't change logging.NewTransport to NewSubsystemLoggingHTTPTransport until all resources are in TPF. + return logging.NewTransport("Atlas", base) } -type SecretData struct { - PublicKey string `json:"public_key"` - PrivateKey string `json:"private_key"` + +// MongoDBClient contains the mongodbatlas clients and configurations. +type MongoDBClient struct { + Atlas *matlasClient.Client + AtlasV2 *admin.APIClient + AtlasPreview *adminpreview.APIClient + AtlasV220240530 *admin20240530.APIClient // Used in cluster to support deprecated attributes default_read_concern and fail_index_key_too_long in advanced_configuration. + AtlasV220241113 *admin20241113.APIClient // Used in teams and atlas_users to avoiding breaking changes. + Realm *RealmClient + BaseURL string // Needed by organization resource. + TerraformVersion string // Needed by organization resource. } -type UAMetadata struct { - Name string - Value string +type RealmClient struct { + publicKey string + privateKey string + realmBaseURL string + terraformVersion string } -func (c *Config) NewClient(ctx context.Context) (*MongoDBClient, error) { - // Network Logging transport is before Digest transport so it can log the first Digest requests with 401 Unauthorized. - // Terraform logging transport is after Digest transport so the Unauthorized request bodies are not logged. - networkLoggingTransport := NewTransportWithNetworkLogging(baseTransport, logging.IsDebugOrHigher()) - digestTransport := digest.NewTransportWithHTTPRoundTripper(cast.ToString(c.PublicKey), cast.ToString(c.PrivateKey), networkLoggingTransport) - // Don't change logging.NewTransport to NewSubsystemLoggingHTTPTransport until all resources are in TPF. - tfLoggingTransport := logging.NewTransport("Atlas", digestTransport) - client := &http.Client{Transport: tfLoggingTransport} +func NewClient(c *Credentials, terraformVersion string) (*MongoDBClient, error) { + userAgent := userAgent(terraformVersion) + client, err := getHTTPClient(c) + if err != nil { + return nil, err + } - optsAtlas := []matlasClient.ClientOpt{matlasClient.SetUserAgent(userAgent(c))} + // Initialize the old SDK + optsAtlas := []matlasClient.ClientOpt{matlasClient.SetUserAgent(userAgent)} if c.BaseURL != "" { optsAtlas = append(optsAtlas, matlasClient.SetBaseURL(c.BaseURL)) } - - // Initialize the MongoDB Atlas API Client. atlasClient, err := matlasClient.New(client, optsAtlas...) if err != nil { return nil, err } - sdkV2Client, err := c.newSDKV2Client(client) + // Initialize the new SDK for different versions + sdkV2Client, err := newSDKV2Client(client, c.BaseURL, userAgent) if err != nil { return nil, err } - - sdkPreviewClient, err := c.newSDKPreviewClient(client) + sdkPreviewClient, err := newSDKPreviewClient(client, c.BaseURL, userAgent) if err != nil { return nil, err } - - sdkV220240530Client, err := c.newSDKV220240530Client(client) + sdkV220240530Client, err := newSDKV220240530Client(client, c.BaseURL, userAgent) if err != nil { return nil, err } - - sdkV220240805Client, err := c.newSDKV220240805Client(client) - if err != nil { - return nil, err - } - - sdkV220241113Client, err := c.newSDKV220241113Client(client) + sdkV220241113Client, err := newSDKV220241113Client(client, c.BaseURL, userAgent) if err != nil { return nil, err } clients := &MongoDBClient{ - Atlas: atlasClient, - AtlasV2: sdkV2Client, - AtlasPreview: sdkPreviewClient, - AtlasV220240530: sdkV220240530Client, - AtlasV220240805: sdkV220240805Client, - AtlasV220241113: sdkV220241113Client, - Config: c, + Atlas: atlasClient, + AtlasV2: sdkV2Client, + AtlasPreview: sdkPreviewClient, + AtlasV220240530: sdkV220240530Client, + AtlasV220241113: sdkV220241113Client, + BaseURL: c.BaseURL, + TerraformVersion: terraformVersion, + Realm: &RealmClient{ + publicKey: c.PublicKey, + privateKey: c.PrivateKey, + realmBaseURL: NormalizeBaseURL(c.RealmBaseURL), + terraformVersion: terraformVersion, + }, } return clients, nil } -func (c *Config) newSDKV2Client(client *http.Client) (*admin.APIClient, error) { - opts := []admin.ClientModifier{ - admin.UseHTTPClient(client), - admin.UseUserAgent(userAgent(c)), - admin.UseBaseURL(c.BaseURL), - admin.UseDebug(false)} - - sdk, err := admin.NewClient(opts...) - if err != nil { - return nil, err +func getHTTPClient(c *Credentials) (*http.Client, error) { + transport := networkLoggingBaseTransport() + switch c.AuthMethod() { + case AccessToken: + tokenSource := oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: c.AccessToken, + TokenType: "Bearer", // Use a static bearer token with oauth2 transport. + }) + transport = &oauth2.Transport{ + Source: tokenSource, + Base: networkLoggingBaseTransport(), + } + case ServiceAccount: + tokenSource, err := getTokenSource(c.ClientID, c.ClientSecret, c.BaseURL, networkLoggingBaseTransport()) + if err != nil { + return nil, err + } + transport = &oauth2.Transport{ + Source: tokenSource, + Base: networkLoggingBaseTransport(), + } + case Digest: + transport = digest.NewTransportWithHTTPRoundTripper(c.PublicKey, c.PrivateKey, networkLoggingBaseTransport()) + case Unknown: } - return sdk, nil + return &http.Client{Transport: tfLoggingInterceptor(transport)}, nil } -func (c *Config) newSDKPreviewClient(client *http.Client) (*adminpreview.APIClient, error) { - opts := []adminpreview.ClientModifier{ - adminpreview.UseHTTPClient(client), - adminpreview.UseUserAgent(userAgent(c)), - adminpreview.UseBaseURL(c.BaseURL), - adminpreview.UseDebug(false)} - - sdk, err := adminpreview.NewClient(opts...) - if err != nil { - return nil, err - } - return sdk, nil +func newSDKV2Client(client *http.Client, baseURL, userAgent string) (*admin.APIClient, error) { + return admin.NewClient( + admin.UseHTTPClient(client), + admin.UseUserAgent(userAgent), + admin.UseBaseURL(baseURL), + admin.UseDebug(false), + ) } -func (c *Config) newSDKV220240530Client(client *http.Client) (*admin20240530.APIClient, error) { - opts := []admin20240530.ClientModifier{ - admin20240530.UseHTTPClient(client), - admin20240530.UseUserAgent(userAgent(c)), - admin20240530.UseBaseURL(c.BaseURL), - admin20240530.UseDebug(false)} - - sdk, err := admin20240530.NewClient(opts...) - if err != nil { - return nil, err - } - return sdk, nil +func newSDKPreviewClient(client *http.Client, baseURL, userAgent string) (*adminpreview.APIClient, error) { + return adminpreview.NewClient( + adminpreview.UseHTTPClient(client), + adminpreview.UseUserAgent(userAgent), + adminpreview.UseBaseURL(baseURL), + adminpreview.UseDebug(false), + ) } -func (c *Config) newSDKV220240805Client(client *http.Client) (*admin20240805.APIClient, error) { - opts := []admin20240805.ClientModifier{ - admin20240805.UseHTTPClient(client), - admin20240805.UseUserAgent(userAgent(c)), - admin20240805.UseBaseURL(c.BaseURL), - admin20240805.UseDebug(false)} - - sdk, err := admin20240805.NewClient(opts...) - if err != nil { - return nil, err - } - return sdk, nil +func newSDKV220240530Client(client *http.Client, baseURL, userAgent string) (*admin20240530.APIClient, error) { + return admin20240530.NewClient( + admin20240530.UseHTTPClient(client), + admin20240530.UseUserAgent(userAgent), + admin20240530.UseBaseURL(baseURL), + admin20240530.UseDebug(false), + ) } -func (c *Config) newSDKV220241113Client(client *http.Client) (*admin20241113.APIClient, error) { - opts := []admin20241113.ClientModifier{ +func newSDKV220241113Client(client *http.Client, baseURL, userAgent string) (*admin20241113.APIClient, error) { + return admin20241113.NewClient( admin20241113.UseHTTPClient(client), - admin20241113.UseUserAgent(userAgent(c)), - admin20241113.UseBaseURL(c.BaseURL), - admin20241113.UseDebug(false)} - - sdk, err := admin20241113.NewClient(opts...) - if err != nil { - return nil, err - } - return sdk, nil + admin20241113.UseUserAgent(userAgent), + admin20241113.UseBaseURL(baseURL), + admin20241113.UseDebug(false), + ) } -func (c *MongoDBClient) GetRealmClient(ctx context.Context) (*realm.Client, error) { - // Realm - if c.Config.PublicKey == "" && c.Config.PrivateKey == "" { +// Get in RealmClient is a method instead of Atlas fields so it's lazy initialized as it needs a roundtrip to authenticate. +func (r *RealmClient) Get(ctx context.Context) (*realm.Client, error) { + if r.publicKey == "" && r.privateKey == "" { return nil, errors.New("please set `public_key` and `private_key` in order to use the realm client") } - optsRealm := []realm.ClientOpt{realm.SetUserAgent(userAgent(c.Config))} + optsRealm := []realm.ClientOpt{ + realm.SetUserAgent(userAgent(r.terraformVersion)), + } authConfig := realmAuth.NewConfig(nil) - if c.Config.BaseURL != "" && c.Config.RealmBaseURL != "" { - adminURL := c.Config.RealmBaseURL + "api/admin/v3.0/" + if r.realmBaseURL != "" { + adminURL := r.realmBaseURL + "/api/admin/v3.0/" optsRealm = append(optsRealm, realm.SetBaseURL(adminURL)) authConfig.AuthURL, _ = url.Parse(adminURL + "auth/providers/mongodb-cloud/login") } - token, err := authConfig.NewTokenFromCredentials(ctx, c.Config.PublicKey, c.Config.PrivateKey) + token, err := authConfig.NewTokenFromCredentials(ctx, r.publicKey, r.privateKey) if err != nil { return nil, err } @@ -289,20 +287,21 @@ func (c *MongoDBClient) UntypedAPICall(ctx context.Context, params *APICallParam return apiResp, err } -func userAgent(c *Config) string { - isPreviewV2AdvancedClusterEnabled := c.PreviewV2AdvancedClusterEnabled - - metadata := []UAMetadata{ +func userAgent(terraformVersion string) string { + metadata := []struct { + Name string + Value string + }{ {toolName, version.ProviderVersion}, - {terraformPlatformName, c.TerraformVersion}, - {previewV2AdvancedClusterEnabledUAKey, strconv.FormatBool(isPreviewV2AdvancedClusterEnabled)}, + {terraformPlatformName, terraformVersion}, } - var parts []string for _, info := range metadata { + if info.Value == "" { + continue + } part := fmt.Sprintf("%s/%s", info.Name, info.Value) parts = append(parts, part) } - return strings.Join(parts, " ") } diff --git a/internal/config/credentials.go b/internal/config/credentials.go new file mode 100644 index 0000000000..2927cf1201 --- /dev/null +++ b/internal/config/credentials.go @@ -0,0 +1,206 @@ +package config + +import ( + "os" + + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" +) + +// Credentials has all the authentication fields, it also matches with fields that can be stored in AWS Secrets Manager. +type Credentials struct { + AccessToken string `json:"access_token"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + PublicKey string `json:"public_key"` + PrivateKey string `json:"private_key"` + BaseURL string `json:"base_url"` + RealmBaseURL string `json:"realm_base_url"` +} + +// GetCredentials follows the order of AWS Secrets Manager, provider vars and env vars. +func GetCredentials(providerVars, envVars *Vars, getAWSCredentials func(*AWSVars) (*Credentials, error)) (*Credentials, error) { + if awsVars := CoalesceAWSVars(providerVars.GetAWS(), envVars.GetAWS()); awsVars != nil { + awsCredentials, err := getAWSCredentials(awsVars) + if err != nil { + return nil, err + } + return awsCredentials, nil + } + if c := CoalesceCredentials(providerVars.GetCredentials(), envVars.GetCredentials()); c != nil { + return c, nil + } + return &Credentials{}, nil +} + +// AuthMethod follows the order of token, SA and PAK. +func (c *Credentials) AuthMethod() AuthMethod { + switch { + case c.HasAccessToken(): + return AccessToken + case c.HasServiceAccount(): + return ServiceAccount + case c.HasDigest(): + return Digest + default: + return Unknown + } +} + +func (c *Credentials) HasAccessToken() bool { + return c.AccessToken != "" +} + +func (c *Credentials) HasServiceAccount() bool { + return c.ClientID != "" || c.ClientSecret != "" +} + +func (c *Credentials) HasDigest() bool { + return c.PublicKey != "" || c.PrivateKey != "" +} + +func (c *Credentials) IsPresent() bool { + return c.AuthMethod() != Unknown +} + +func (c *Credentials) Warnings() string { + if !c.IsPresent() { + return "No credentials set" + } + // Prefer specific checks over generic code as there are few combinations and code is clearer. + if c.HasAccessToken() && c.HasServiceAccount() && c.HasDigest() { + return "Access Token will be used although Service Account and API Keys are also set" + } + if c.HasAccessToken() && c.HasServiceAccount() { + return "Access Token will be used although Service Account is also set" + } + if c.HasAccessToken() && c.HasDigest() { + return "Access Token will be used although API Key is also set" + } + if c.HasServiceAccount() && c.HasDigest() { + return "Service Account will be used although API Key is also set" + } + return "" +} + +func (c *Credentials) Errors() string { + switch c.AuthMethod() { + case ServiceAccount: + if c.ClientID == "" { + return "Service Account will be used but Client ID is required" + } + if c.ClientSecret == "" { + return "Service Account will be used but Client Secret is required" + } + case Digest: + if c.PublicKey == "" { + return "API Key will be used but Public Key is required" + } + if c.PrivateKey == "" { + return "API Key will be used but Private Key is required" + } + case Unknown, AccessToken: + } + return "" +} + +type AWSVars struct { + AssumeRoleARN string + SecretName string + Region string + AccessKeyID string + SecretAccessKey string + SessionToken string + Endpoint string +} + +func (a *AWSVars) IsPresent() bool { + return a.AssumeRoleARN != "" +} + +type Vars struct { + AccessToken string + ClientID string + ClientSecret string + PublicKey string + PrivateKey string + BaseURL string + RealmBaseURL string + AWSAssumeRoleARN string + AWSSecretName string + AWSRegion string + AWSAccessKeyID string + AWSSecretAccessKey string + AWSSessionToken string + AWSEndpoint string +} + +func NewEnvVars() *Vars { + return &Vars{ + AccessToken: getEnv("MONGODB_ATLAS_ACCESS_TOKEN"), + ClientID: getEnv("MONGODB_ATLAS_CLIENT_ID"), + ClientSecret: getEnv("MONGODB_ATLAS_CLIENT_SECRET"), + PublicKey: getEnv("MONGODB_ATLAS_PUBLIC_API_KEY", "MONGODB_ATLAS_PUBLIC_KEY", "MCLI_PUBLIC_API_KEY"), + PrivateKey: getEnv("MONGODB_ATLAS_PRIVATE_API_KEY", "MONGODB_ATLAS_PRIVATE_KEY", "MCLI_PRIVATE_API_KEY"), + BaseURL: getEnv("MONGODB_ATLAS_BASE_URL", "MCLI_OPS_MANAGER_URL"), + RealmBaseURL: getEnv("MONGODB_REALM_BASE_URL"), + AWSAssumeRoleARN: getEnv("ASSUME_ROLE_ARN", "TF_VAR_ASSUME_ROLE_ARN"), + AWSSecretName: getEnv("SECRET_NAME", "TF_VAR_SECRET_NAME"), + AWSRegion: getEnv("AWS_REGION", "TF_VAR_AWS_REGION"), + AWSAccessKeyID: getEnv("AWS_ACCESS_KEY_ID", "TF_VAR_AWS_ACCESS_KEY_ID"), + AWSSecretAccessKey: getEnv("AWS_SECRET_ACCESS_KEY", "TF_VAR_AWS_SECRET_ACCESS_KEY"), + AWSSessionToken: getEnv("AWS_SESSION_TOKEN", "TF_VAR_AWS_SESSION_TOKEN"), + AWSEndpoint: getEnv("STS_ENDPOINT", "TF_VAR_STS_ENDPOINT"), + } +} + +func (e *Vars) GetCredentials() *Credentials { + return &Credentials{ + AccessToken: e.AccessToken, + ClientID: e.ClientID, + ClientSecret: e.ClientSecret, + PublicKey: e.PublicKey, + PrivateKey: e.PrivateKey, + BaseURL: e.BaseURL, + RealmBaseURL: e.RealmBaseURL, + } +} + +// GetAWS returns variables in the format AWS expects, e.g. region in lowercase. +func (e *Vars) GetAWS() *AWSVars { + return &AWSVars{ + AssumeRoleARN: e.AWSAssumeRoleARN, + SecretName: e.AWSSecretName, + Region: conversion.MongoDBRegionToAWSRegion(e.AWSRegion), + AccessKeyID: e.AWSAccessKeyID, + SecretAccessKey: e.AWSSecretAccessKey, + SessionToken: e.AWSSessionToken, + Endpoint: e.AWSEndpoint, + } +} + +func getEnv(key ...string) string { + for _, k := range key { + if v := os.Getenv(k); v != "" { + return v + } + } + return "" +} + +func CoalesceAWSVars(awsVars ...*AWSVars) *AWSVars { + for _, awsVar := range awsVars { + if awsVar.IsPresent() { + return awsVar + } + } + return nil +} + +func CoalesceCredentials(credentials ...*Credentials) *Credentials { + for _, credential := range credentials { + if credential.IsPresent() { + return credential + } + } + return nil +} diff --git a/internal/config/credentials_test.go b/internal/config/credentials_test.go new file mode 100644 index 0000000000..f690ad7b54 --- /dev/null +++ b/internal/config/credentials_test.go @@ -0,0 +1,611 @@ +package config_test + +import ( + "errors" + "testing" + + "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCredentials_AuthMethod(t *testing.T) { + testCases := map[string]struct { + credentials config.Credentials + want config.AuthMethod + }{ + "Empty credentials returns Unknown": { + credentials: config.Credentials{}, + want: config.Unknown, + }, + "Access token takes priority": { + credentials: config.Credentials{ + AccessToken: "token", + ClientID: "id", + ClientSecret: "secret", + PublicKey: "public", + PrivateKey: "private", + }, + want: config.AccessToken, + }, + "Service account when no access token": { + credentials: config.Credentials{ + ClientID: "id", + ClientSecret: "secret", + PublicKey: "public", + PrivateKey: "private", + }, + want: config.ServiceAccount, + }, + "Service account with only ClientID": { + credentials: config.Credentials{ + ClientID: "id", + }, + want: config.ServiceAccount, + }, + "Service account with only ClientSecret": { + credentials: config.Credentials{ + ClientSecret: "secret", + }, + want: config.ServiceAccount, + }, + "Digest when only digest credentials": { + credentials: config.Credentials{ + PublicKey: "public", + PrivateKey: "private", + }, + want: config.Digest, + }, + "Digest with only PublicKey": { + credentials: config.Credentials{ + PublicKey: "public", + }, + want: config.Digest, + }, + "Digest with only PrivateKey": { + credentials: config.Credentials{ + PrivateKey: "private", + }, + want: config.Digest, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := tc.credentials.AuthMethod() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestCredentials_HasAccessToken(t *testing.T) { + testCases := map[string]struct { + credentials config.Credentials + want bool + }{ + "Empty credentials": { + credentials: config.Credentials{}, + want: false, + }, + "With access token": { + credentials: config.Credentials{ + AccessToken: "token", + }, + want: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := tc.credentials.HasAccessToken() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestCredentials_HasServiceAccount(t *testing.T) { + testCases := map[string]struct { + credentials config.Credentials + want bool + }{ + "Empty credentials": { + credentials: config.Credentials{}, + want: false, + }, + "With ClientID only": { + credentials: config.Credentials{ + ClientID: "id", + }, + want: true, + }, + "With ClientSecret only": { + credentials: config.Credentials{ + ClientSecret: "secret", + }, + want: true, + }, + "With both ClientID and ClientSecret": { + credentials: config.Credentials{ + ClientID: "id", + ClientSecret: "secret", + }, + want: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := tc.credentials.HasServiceAccount() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestCredentials_HasDigest(t *testing.T) { + testCases := map[string]struct { + credentials config.Credentials + want bool + }{ + "Empty credentials": { + credentials: config.Credentials{}, + want: false, + }, + "With PublicKey only": { + credentials: config.Credentials{ + PublicKey: "public", + }, + want: true, + }, + "With PrivateKey only": { + credentials: config.Credentials{ + PrivateKey: "private", + }, + want: true, + }, + "With both PublicKey and PrivateKey": { + credentials: config.Credentials{ + PublicKey: "public", + PrivateKey: "private", + }, + want: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := tc.credentials.HasDigest() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestCredentials_IsPresent(t *testing.T) { + testCases := map[string]struct { + credentials config.Credentials + want bool + }{ + "Empty credentials": { + credentials: config.Credentials{}, + want: false, + }, + "With access token": { + credentials: config.Credentials{ + AccessToken: "token", + }, + want: true, + }, + "With service account": { + credentials: config.Credentials{ + ClientID: "id", + }, + want: true, + }, + "With digest": { + credentials: config.Credentials{ + PublicKey: "public", + }, + want: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := tc.credentials.IsPresent() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestCredentials_Warnings(t *testing.T) { + testCases := map[string]struct { + credentials config.Credentials + want string + }{ + "No credentials": { + credentials: config.Credentials{}, + want: "No credentials set", + }, + "Only access token - no warning": { + credentials: config.Credentials{ + AccessToken: "token", + }, + want: "", + }, + "Only service account - no warning": { + credentials: config.Credentials{ + ClientID: "id", + }, + want: "", + }, + "Only digest - no warning": { + credentials: config.Credentials{ + PublicKey: "public", + }, + want: "", + }, + "Access token and service account": { + credentials: config.Credentials{ + AccessToken: "token", + ClientID: "id", + ClientSecret: "secret", + }, + want: "Access Token will be used although Service Account is also set", + }, + "Access token and digest": { + credentials: config.Credentials{ + AccessToken: "token", + PublicKey: "public", + PrivateKey: "private", + }, + want: "Access Token will be used although API Key is also set", + }, + "Service account and digest": { + credentials: config.Credentials{ + ClientID: "id", + PublicKey: "public", + PrivateKey: "private", + }, + want: "Service Account will be used although API Key is also set", + }, + "All three methods": { + credentials: config.Credentials{ + AccessToken: "token", + ClientID: "id", + ClientSecret: "secret", + PublicKey: "public", + PrivateKey: "private", + }, + want: "Access Token will be used although Service Account and API Keys are also set", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := tc.credentials.Warnings() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestCredentials_Errors(t *testing.T) { + testCases := map[string]struct { + credentials config.Credentials + want string + }{ + "No credentials - no error": { + credentials: config.Credentials{}, + want: "", + }, + "Valid access token - no error": { + credentials: config.Credentials{ + AccessToken: "token", + }, + want: "", + }, + "Service account missing ClientID": { + credentials: config.Credentials{ + ClientSecret: "secret", + }, + want: "Service Account will be used but Client ID is required", + }, + "Service account missing ClientSecret": { + credentials: config.Credentials{ + ClientID: "id", + }, + want: "Service Account will be used but Client Secret is required", + }, + "Service account with both - no error": { + credentials: config.Credentials{ + ClientID: "id", + ClientSecret: "secret", + }, + want: "", + }, + "Digest missing PublicKey": { + credentials: config.Credentials{ + PrivateKey: "private", + }, + want: "API Key will be used but Public Key is required", + }, + "Digest missing PrivateKey": { + credentials: config.Credentials{ + PublicKey: "public", + }, + want: "API Key will be used but Private Key is required", + }, + "Digest with both - no error": { + credentials: config.Credentials{ + PublicKey: "public", + PrivateKey: "private", + }, + want: "", + }, + "Access token takes priority - no error even with incomplete service account": { + credentials: config.Credentials{ + AccessToken: "token", + ClientID: "id", + // Missing ClientSecret, but should not error since AccessToken takes priority + }, + want: "", + }, + "Access token takes priority - no error even with incomplete digest": { + credentials: config.Credentials{ + AccessToken: "token", + PublicKey: "public", + // Missing PrivateKey, but should not error since AccessToken takes priority + }, + want: "", + }, + "Service account takes priority over incomplete digest": { + credentials: config.Credentials{ + ClientID: "id", + ClientSecret: "secret", + PublicKey: "public", + // Missing PrivateKey, but should not error since ServiceAccount takes priority + }, + want: "", + }, + "Service account incomplete but takes priority over digest": { + credentials: config.Credentials{ + ClientID: "id", + // Missing ClientSecret + PublicKey: "public", + PrivateKey: "private", + }, + want: "Service Account will be used but Client Secret is required", + }, + "All credentials present - no error": { + credentials: config.Credentials{ + AccessToken: "token", + ClientID: "id", + ClientSecret: "secret", + PublicKey: "public", + PrivateKey: "private", + }, + want: "", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := tc.credentials.Errors() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestGetCredentials(t *testing.T) { + mockGetAWSCredentials := func(awsVars *config.AWSVars) (*config.Credentials, error) { + if awsVars.AssumeRoleARN == "error" { + return nil, errors.New("AWS error") + } + return &config.Credentials{ + AccessToken: "aws-token", + }, nil + } + + testCases := map[string]struct { + providerVars *config.Vars + envVars *config.Vars + want *config.Credentials + wantErr bool + }{ + "AWS credentials take priority": { + providerVars: &config.Vars{ + AWSAssumeRoleARN: "arn", + PublicKey: "provider-public", + }, + envVars: &config.Vars{ + PublicKey: "env-public", + }, + want: &config.Credentials{ + AccessToken: "aws-token", + }, + wantErr: false, + }, + "AWS credentials error": { + providerVars: &config.Vars{ + AWSAssumeRoleARN: "error", + }, + envVars: &config.Vars{}, + want: nil, + wantErr: true, + }, + "Provider vars take priority over env vars": { + providerVars: &config.Vars{ + PublicKey: "provider-public", + }, + envVars: &config.Vars{ + PublicKey: "env-public", + }, + want: &config.Credentials{ + PublicKey: "provider-public", + }, + wantErr: false, + }, + "Env vars when no provider vars": { + providerVars: &config.Vars{}, + envVars: &config.Vars{ + PublicKey: "env-public", + }, + want: &config.Credentials{ + PublicKey: "env-public", + }, + wantErr: false, + }, + "Empty credentials when nothing provided": { + providerVars: &config.Vars{}, + envVars: &config.Vars{}, + want: &config.Credentials{}, + wantErr: false, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, err := config.GetCredentials(tc.providerVars, tc.envVars, mockGetAWSCredentials) + if tc.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.want, got) + } + }) + } +} + +func TestAWSVars_IsPresent(t *testing.T) { + testCases := map[string]struct { + awsVars *config.AWSVars + want bool + }{ + "Empty AWS vars": { + awsVars: &config.AWSVars{}, + want: false, + }, + "With AssumeRoleARN": { + awsVars: &config.AWSVars{ + AssumeRoleARN: "arn", + }, + want: true, + }, + "With other fields but no AssumeRoleARN": { + awsVars: &config.AWSVars{ + SecretName: "secret", + Region: "us-east-1", + }, + want: false, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := tc.awsVars.IsPresent() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestNewEnvVars(t *testing.T) { + // Test the first env var for each attribute. + t.Setenv("MONGODB_ATLAS_ACCESS_TOKEN", "env-token") + t.Setenv("MONGODB_ATLAS_CLIENT_ID", "env-client-id") + t.Setenv("MONGODB_ATLAS_CLIENT_SECRET", "env-client-secret") + t.Setenv("MONGODB_ATLAS_PUBLIC_API_KEY", "env-public") + t.Setenv("MONGODB_ATLAS_PRIVATE_API_KEY", "env-private") + t.Setenv("MONGODB_ATLAS_BASE_URL", "url1") + t.Setenv("MONGODB_REALM_BASE_URL", "url2") + t.Setenv("ASSUME_ROLE_ARN", "arn") + t.Setenv("SECRET_NAME", "env-secret") + t.Setenv("AWS_REGION", "us-west-2") + t.Setenv("AWS_ACCESS_KEY_ID", "env-access") + t.Setenv("AWS_SECRET_ACCESS_KEY", "env-secret-key") + t.Setenv("AWS_SESSION_TOKEN", "env-token") + t.Setenv("STS_ENDPOINT", "https://sts.amazonaws.com") + + vars := config.NewEnvVars() + assert.Equal(t, "env-token", vars.AccessToken) + assert.Equal(t, "env-client-id", vars.ClientID) + assert.Equal(t, "env-client-secret", vars.ClientSecret) + assert.Equal(t, "env-public", vars.PublicKey) + assert.Equal(t, "env-private", vars.PrivateKey) + assert.Equal(t, "url1", vars.BaseURL) + assert.Equal(t, "url2", vars.RealmBaseURL) + assert.Equal(t, "arn", vars.AWSAssumeRoleARN) + assert.Equal(t, "env-secret", vars.AWSSecretName) + assert.Equal(t, "us-west-2", vars.AWSRegion) + assert.Equal(t, "env-access", vars.AWSAccessKeyID) + assert.Equal(t, "env-secret-key", vars.AWSSecretAccessKey) + assert.Equal(t, "env-token", vars.AWSSessionToken) + assert.Equal(t, "https://sts.amazonaws.com", vars.AWSEndpoint) +} + +func TestCoalesceAWSVars(t *testing.T) { + awsVars1 := &config.AWSVars{AssumeRoleARN: "arn1"} + awsVars2 := &config.AWSVars{AssumeRoleARN: "arn2"} + awsVarsEmpty := &config.AWSVars{} + + testCases := map[string]struct { + want *config.AWSVars + awsVars []*config.AWSVars + }{ + "First present AWS vars": { + awsVars: []*config.AWSVars{awsVars1, awsVars2}, + want: awsVars1, + }, + "Skip empty, return first present": { + awsVars: []*config.AWSVars{awsVarsEmpty, awsVars2}, + want: awsVars2, + }, + "All empty returns nil": { + awsVars: []*config.AWSVars{awsVarsEmpty, awsVarsEmpty}, + want: nil, + }, + "No vars returns nil": { + awsVars: []*config.AWSVars{}, + want: nil, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := config.CoalesceAWSVars(tc.awsVars...) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestCoalesceCredentials(t *testing.T) { + creds1 := &config.Credentials{PublicKey: "key1"} + creds2 := &config.Credentials{PublicKey: "key2"} + credsEmpty := &config.Credentials{} + + testCases := map[string]struct { + want *config.Credentials + credentials []*config.Credentials + }{ + "First present credentials": { + credentials: []*config.Credentials{creds1, creds2}, + want: creds1, + }, + "Skip empty, return first present": { + credentials: []*config.Credentials{credsEmpty, creds2}, + want: creds2, + }, + "All empty returns nil": { + credentials: []*config.Credentials{credsEmpty, credsEmpty}, + want: nil, + }, + "No credentials returns nil": { + credentials: []*config.Credentials{}, + want: nil, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := config.CoalesceCredentials(tc.credentials...) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/internal/config/service_account.go b/internal/config/service_account.go new file mode 100644 index 0000000000..a2f9d9f63e --- /dev/null +++ b/internal/config/service_account.go @@ -0,0 +1,59 @@ +package config + +import ( + "context" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "github.com/mongodb/atlas-sdk-go/auth" + "github.com/mongodb/atlas-sdk-go/auth/clientcredentials" + "golang.org/x/oauth2" +) + +// Renew token if it expires within 10 minutes to avoid authentication errors during Atlas API calls. +const saTokenExpiryBuffer = 10 * time.Minute + +var saInfo = struct { + tokenSource auth.TokenSource + clientID string + clientSecret string + baseURL string + mu sync.Mutex +}{} + +func getTokenSource(clientID, clientSecret, baseURL string, tokenRenewalBase http.RoundTripper) (auth.TokenSource, error) { + saInfo.mu.Lock() + defer saInfo.mu.Unlock() + + baseURL = NormalizeBaseURL(baseURL) + if saInfo.tokenSource != nil { // Token source in cache. + if saInfo.clientID != clientID || saInfo.clientSecret != clientSecret || saInfo.baseURL != baseURL { + return nil, fmt.Errorf("service account credentials changed") + } + return saInfo.tokenSource, nil + } + + conf := clientcredentials.NewConfig(clientID, clientSecret) + if baseURL != "" { + conf.TokenURL = baseURL + clientcredentials.TokenAPIPath + conf.RevokeURL = baseURL + clientcredentials.RevokeAPIPath + } + // Use a new context to avoid "context canceled" errors as the token source is reused and can outlast the callee context. + ctx := context.WithValue(context.Background(), auth.HTTPClient, &http.Client{Transport: tokenRenewalBase}) + tokenSource := oauth2.ReuseTokenSourceWithExpiry(nil, conf.TokenSource(ctx), saTokenExpiryBuffer) + if _, err := tokenSource.Token(); err != nil { // Retrieve token to fail-fast if credentials are invalid. + return nil, err + } + saInfo.clientID = clientID + saInfo.clientSecret = clientSecret + saInfo.baseURL = baseURL + saInfo.tokenSource = tokenSource + return saInfo.tokenSource, nil +} + +func NormalizeBaseURL(baseURL string) string { + return strings.TrimRight(baseURL, "/") +} diff --git a/internal/config/transport.go b/internal/config/transport.go index 766b6a6360..100ad2872d 100644 --- a/internal/config/transport.go +++ b/internal/config/transport.go @@ -16,10 +16,7 @@ type NetworkLoggingTransport struct { // NewTransportWithNetworkLogging creates a new NetworkLoggingTransport that wraps // the provided transport with enhanced network logging capabilities. -func NewTransportWithNetworkLogging(transport http.RoundTripper, enabled bool) *NetworkLoggingTransport { - if transport == nil { - transport = http.DefaultTransport - } +func NewTransportWithNetworkLogging(transport http.RoundTripper, enabled bool) http.RoundTripper { return &NetworkLoggingTransport{ Transport: transport, Enabled: enabled, diff --git a/internal/config/transport_test.go b/internal/config/transport_test.go index 53908852d0..2c54b5d77f 100644 --- a/internal/config/transport_test.go +++ b/internal/config/transport_test.go @@ -159,15 +159,17 @@ func TestAccNetworkLogging(t *testing.T) { var logOutput bytes.Buffer log.SetOutput(&logOutput) defer log.SetOutput(os.Stderr) - cfg := &config.Config{ - PublicKey: os.Getenv("MONGODB_ATLAS_PUBLIC_KEY"), - PrivateKey: os.Getenv("MONGODB_ATLAS_PRIVATE_KEY"), - BaseURL: os.Getenv("MONGODB_ATLAS_BASE_URL"), + c := &config.Credentials{ + PublicKey: os.Getenv("MONGODB_ATLAS_PUBLIC_KEY"), + PrivateKey: os.Getenv("MONGODB_ATLAS_PRIVATE_KEY"), + ClientID: os.Getenv("MONGODB_ATLAS_CLIENT_ID"), + ClientSecret: os.Getenv("MONGODB_ATLAS_CLIENT_SECRET"), + BaseURL: os.Getenv("MONGODB_ATLAS_BASE_URL"), } - client, err := cfg.NewClient(t.Context()) + client, err := config.NewClient(c, "") require.NoError(t, err) - // Make a simple API call that should trigger our enhanced logging + // Make a simple API call that should trigger our enhanced logging. _, _, err = client.AtlasV2.OrganizationsApi.ListOrgs(t.Context()).Execute() require.NoError(t, err) logStr := logOutput.String() diff --git a/internal/provider/aws_credentials.go b/internal/provider/aws_credentials.go index acd97e14b3..34ceaf93fb 100644 --- a/internal/provider/aws_credentials.go +++ b/internal/provider/aws_credentials.go @@ -25,59 +25,34 @@ const ( minSegmentsForSTSRegionalHost = 4 ) -func configureCredentialsSTS(cfg *config.Config, secret, region, awsAccessKeyID, awsSecretAccessKey, awsSessionToken, endpoint string) (config.Config, error) { +func getAWSCredentials(c *config.AWSVars) (*config.Credentials, error) { defaultResolver := endpoints.DefaultResolver() stsCustResolverFn := func(service, _ string, optFns ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) { if service == sts.EndpointsID { - resolved, err := ResolveSTSEndpoint(endpoint, region) + resolved, err := ResolveSTSEndpoint(c.Endpoint, c.Region) if err != nil { return endpoints.ResolvedEndpoint{}, err } return resolved, nil } - return defaultResolver.EndpointFor(service, region, optFns...) + return defaultResolver.EndpointFor(service, c.Region, optFns...) } - sess := session.Must(session.NewSession(&aws.Config{ - Region: aws.String(region), - Credentials: credentials.NewStaticCredentials(awsAccessKeyID, awsSecretAccessKey, awsSessionToken), + Region: aws.String(c.Region), + Credentials: credentials.NewStaticCredentials(c.AccessKeyID, c.SecretAccessKey, c.SessionToken), EndpointResolver: endpoints.ResolverFunc(stsCustResolverFn), })) - - creds := stscreds.NewCredentials(sess, cfg.AssumeRoleARN) - - _, err := sess.Config.Credentials.Get() - if err != nil { - log.Printf("Session get credentials error: %s", err) - return *cfg, err - } - _, err = creds.Get() - if err != nil { - log.Printf("STS get credentials error: %s", err) - return *cfg, err - } - secretString, err := secretsManagerGetSecretValue(sess, &aws.Config{Credentials: creds, Region: aws.String(region)}, secret) + creds := stscreds.NewCredentials(sess, c.AssumeRoleARN) + secretString, err := secretsManagerGetSecretValue(sess, &aws.Config{Credentials: creds, Region: aws.String(c.Region)}, c.SecretName) if err != nil { - log.Printf("Get Secrets error: %s", err) - return *cfg, err + return nil, err } - - var secretData SecretData - err = json.Unmarshal([]byte(secretString), &secretData) + var secret config.Credentials + err = json.Unmarshal([]byte(secretString), &secret) if err != nil { - return *cfg, err - } - if secretData.PrivateKey == "" { - return *cfg, fmt.Errorf("secret missing value for credential PrivateKey") + return nil, err } - - if secretData.PublicKey == "" { - return *cfg, fmt.Errorf("secret missing value for credential PublicKey") - } - - cfg.PublicKey = secretData.PublicKey - cfg.PrivateKey = secretData.PrivateKey - return *cfg, nil + return &secret, nil } func DeriveSTSRegionFromEndpoint(ep string) string { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 81d6ef19dc..3b3a32ef71 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -3,12 +3,10 @@ package provider import ( "context" "log" - "os" + "slices" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -20,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-mux/tf5to6server" "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" - "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/advancedcluster" "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/alertconfiguration" @@ -53,11 +50,8 @@ import ( ) const ( - MongodbGovCloudURL = "https://cloud.mongodbgov.com" - MongodbGovCloudQAURL = "https://cloud-qa.mongodbgov.com" - MongodbGovCloudDevURL = "https://cloud-dev.mongodbgov.com" ProviderConfigError = "error in configuring the provider." - MissingAuthAttrError = "either Atlas Programmatic API Keys or AWS Secrets Manager attributes must be set" + MissingAuthAttrError = "either AWS Secrets Manager, Service Accounts or Atlas Programmatic API Keys attributes must be set" ProviderMetaUserAgentExtra = "user_agent_extra" ProviderMetaUserAgentExtraDesc = "You can extend the user agent header for each request made by the provider to the Atlas Admin API. The Key Values will be formatted as {key}/{value}." ProviderMetaModuleName = "module_name" @@ -69,29 +63,28 @@ const ( type MongodbtlasProvider struct { } -type tfMongodbAtlasProviderModel struct { - AssumeRole types.List `tfsdk:"assume_role"` - PublicKey types.String `tfsdk:"public_key"` - PrivateKey types.String `tfsdk:"private_key"` - BaseURL types.String `tfsdk:"base_url"` - RealmBaseURL types.String `tfsdk:"realm_base_url"` - SecretName types.String `tfsdk:"secret_name"` - Region types.String `tfsdk:"region"` - StsEndpoint types.String `tfsdk:"sts_endpoint"` - AwsAccessKeyID types.String `tfsdk:"aws_access_key_id"` - AwsSecretAccessKeyID types.String `tfsdk:"aws_secret_access_key"` - AwsSessionToken types.String `tfsdk:"aws_session_token"` - IsMongodbGovCloud types.Bool `tfsdk:"is_mongodbgov_cloud"` +type tfModel struct { + Region types.String `tfsdk:"region"` + PrivateKey types.String `tfsdk:"private_key"` + BaseURL types.String `tfsdk:"base_url"` + RealmBaseURL types.String `tfsdk:"realm_base_url"` + SecretName types.String `tfsdk:"secret_name"` + PublicKey types.String `tfsdk:"public_key"` + StsEndpoint types.String `tfsdk:"sts_endpoint"` + AwsAccessKeyID types.String `tfsdk:"aws_access_key_id"` + AwsSecretAccessKeyID types.String `tfsdk:"aws_secret_access_key"` + AwsSessionToken types.String `tfsdk:"aws_session_token"` + ClientID types.String `tfsdk:"client_id"` + ClientSecret types.String `tfsdk:"client_secret"` + AccessToken types.String `tfsdk:"access_token"` + AssumeRole []tfAssumeRoleModel `tfsdk:"assume_role"` + IsMongodbGovCloud types.Bool `tfsdk:"is_mongodbgov_cloud"` } type tfAssumeRoleModel struct { RoleARN types.String `tfsdk:"role_arn"` } -var AssumeRoleType = types.ObjectType{AttrTypes: map[string]attr.Type{ - "role_arn": types.StringType, -}} - func (p *MongodbtlasProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { resp.TypeName = "mongodbatlas" resp.Version = version.ProviderVersion @@ -168,6 +161,18 @@ func (p *MongodbtlasProvider) Schema(ctx context.Context, req provider.SchemaReq Optional: true, Description: "AWS Security Token Service provided session token.", }, + "client_id": schema.StringAttribute{ + Optional: true, + Description: "MongoDB Atlas Client ID for Service Account.", + }, + "client_secret": schema.StringAttribute{ + Optional: true, + Description: "MongoDB Atlas Client Secret for Service Account.", + }, + "access_token": schema.StringAttribute{ + Optional: true, + Description: "MongoDB Atlas Access Token for Service Account.", + }, }, } } @@ -185,165 +190,70 @@ var fwAssumeRoleSchema = schema.ListNestedBlock{ } func (p *MongodbtlasProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { - var data tfMongodbAtlasProviderModel - - resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + providerVars := getProviderVars(ctx, req, resp) if resp.Diagnostics.HasError() { return } - - data = setDefaultValuesWithValidations(ctx, &data, resp) - if resp.Diagnostics.HasError() { + c, err := config.GetCredentials(providerVars, config.NewEnvVars(), getAWSCredentials) + if err != nil { + resp.Diagnostics.AddError("Error getting credentials for provider", err.Error()) return } - - cfg := config.Config{ - PublicKey: data.PublicKey.ValueString(), - PrivateKey: data.PrivateKey.ValueString(), - BaseURL: data.BaseURL.ValueString(), - RealmBaseURL: data.RealmBaseURL.ValueString(), - TerraformVersion: req.TerraformVersion, + if c.Errors() != "" { + resp.Diagnostics.AddError("Error getting credentials for provider", c.Errors()) + return } - - var assumeRoles []tfAssumeRoleModel - data.AssumeRole.ElementsAs(ctx, &assumeRoles, true) - awsRoleDefined := len(assumeRoles) > 0 - if awsRoleDefined { - cfg.AssumeRoleARN = assumeRoles[0].RoleARN.ValueString() - secret := data.SecretName.ValueString() - region := conversion.MongoDBRegionToAWSRegion(data.Region.ValueString()) - awsAccessKeyID := data.AwsAccessKeyID.ValueString() - awsSecretAccessKey := data.AwsSecretAccessKeyID.ValueString() - awsSessionToken := data.AwsSessionToken.ValueString() - endpoint := data.StsEndpoint.ValueString() - var err error - cfg, err = configureCredentialsSTS(&cfg, secret, region, awsAccessKeyID, awsSecretAccessKey, awsSessionToken, endpoint) - if err != nil { - resp.Diagnostics.AddError("failed to configure credentials STS", err.Error()) - return - } + if c.Warnings() != "" { + resp.Diagnostics.AddWarning("Warning getting credentials for provider", c.Warnings()) } - - client, err := cfg.NewClient(ctx) - + client, err := config.NewClient(c, req.TerraformVersion) if err != nil { - resp.Diagnostics.AddError( - "failed to initialize a new client", - err.Error(), - ) + resp.Diagnostics.AddError("Error initializing provider", err.Error()) return } - resp.DataSourceData = client resp.ResourceData = client } -func setDefaultValuesWithValidations(ctx context.Context, data *tfMongodbAtlasProviderModel, resp *provider.ConfigureResponse) tfMongodbAtlasProviderModel { - if mongodbgovCloud := data.IsMongodbGovCloud.ValueBool(); mongodbgovCloud { - if !isGovBaseURLConfiguredForProvider(data) { - data.BaseURL = types.StringValue(MongodbGovCloudURL) - } - } - if data.BaseURL.ValueString() == "" { - data.BaseURL = types.StringValue(MultiEnvDefaultFunc([]string{ - "MONGODB_ATLAS_BASE_URL", - "MCLI_OPS_MANAGER_URL", - }, "").(string)) - } - - awsRoleDefined := false - if len(data.AssumeRole.Elements()) == 0 { - assumeRoleArn := MultiEnvDefaultFunc([]string{ - "ASSUME_ROLE_ARN", - "TF_VAR_ASSUME_ROLE_ARN", - }, "").(string) - if assumeRoleArn != "" { - awsRoleDefined = true - var diags diag.Diagnostics - data.AssumeRole, diags = types.ListValueFrom(ctx, AssumeRoleType, []tfAssumeRoleModel{ - { - RoleARN: types.StringValue(assumeRoleArn), - }, - }) - if diags.HasError() { - resp.Diagnostics.Append(diags...) - } - } - } else { - awsRoleDefined = true - } - - if data.PublicKey.ValueString() == "" { - data.PublicKey = types.StringValue(MultiEnvDefaultFunc([]string{ - "MONGODB_ATLAS_PUBLIC_API_KEY", - "MONGODB_ATLAS_PUBLIC_KEY", - "MCLI_PUBLIC_API_KEY", - }, "").(string)) - if data.PublicKey.ValueString() == "" && !awsRoleDefined { - resp.Diagnostics.AddWarning(ProviderConfigError, MissingAuthAttrError) - } - } - - if data.PrivateKey.ValueString() == "" { - data.PrivateKey = types.StringValue(MultiEnvDefaultFunc([]string{ - "MONGODB_ATLAS_PRIVATE_API_KEY", - "MONGODB_ATLAS_PRIVATE_KEY", - "MCLI_PRIVATE_API_KEY", - }, "").(string)) - if data.PrivateKey.ValueString() == "" && !awsRoleDefined { - resp.Diagnostics.AddWarning(ProviderConfigError, MissingAuthAttrError) - } - } - - if data.RealmBaseURL.ValueString() == "" { - data.RealmBaseURL = types.StringValue(MultiEnvDefaultFunc([]string{ - "MONGODB_REALM_BASE_URL", - }, "").(string)) - } - - if data.Region.ValueString() == "" { - data.Region = types.StringValue(MultiEnvDefaultFunc([]string{ - "AWS_REGION", - "TF_VAR_AWS_REGION", - }, "").(string)) - } - - if data.StsEndpoint.ValueString() == "" { - data.StsEndpoint = types.StringValue(MultiEnvDefaultFunc([]string{ - "STS_ENDPOINT", - "TF_VAR_STS_ENDPOINT", - }, "").(string)) +func getProviderVars(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) *config.Vars { + var data tfModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return nil } - - if data.AwsAccessKeyID.ValueString() == "" { - data.AwsAccessKeyID = types.StringValue(MultiEnvDefaultFunc([]string{ - "AWS_ACCESS_KEY_ID", - "TF_VAR_AWS_ACCESS_KEY_ID", - }, "").(string)) + assumeRoleARN := "" + if len(data.AssumeRole) > 0 { + assumeRoleARN = data.AssumeRole[0].RoleARN.ValueString() } - - if data.AwsSecretAccessKeyID.ValueString() == "" { - data.AwsSecretAccessKeyID = types.StringValue(MultiEnvDefaultFunc([]string{ - "AWS_SECRET_ACCESS_KEY", - "TF_VAR_AWS_SECRET_ACCESS_KEY", - }, "").(string)) + baseURL := applyGovBaseURLIfNeeded(data.BaseURL.ValueString(), data.IsMongodbGovCloud.ValueBool()) + return &config.Vars{ + AccessToken: data.AccessToken.ValueString(), + ClientID: data.ClientID.ValueString(), + ClientSecret: data.ClientSecret.ValueString(), + PublicKey: data.PublicKey.ValueString(), + PrivateKey: data.PrivateKey.ValueString(), + BaseURL: baseURL, + RealmBaseURL: data.RealmBaseURL.ValueString(), + AWSAssumeRoleARN: assumeRoleARN, + AWSSecretName: data.SecretName.ValueString(), + AWSRegion: data.Region.ValueString(), + AWSAccessKeyID: data.AwsAccessKeyID.ValueString(), + AWSSecretAccessKey: data.AwsSecretAccessKeyID.ValueString(), + AWSSessionToken: data.AwsSessionToken.ValueString(), + AWSEndpoint: data.StsEndpoint.ValueString(), } +} - if data.AwsSessionToken.ValueString() == "" { - data.AwsSessionToken = types.StringValue(MultiEnvDefaultFunc([]string{ - "AWS_SESSION_TOKEN", - "TF_VAR_AWS_SESSION_TOKEN", - }, "").(string)) +func applyGovBaseURLIfNeeded(providerBaseURL string, providerIsMongodbGovCloud bool) string { + const govURL = "https://cloud.mongodbgov.com" + govAdditionalURLs := []string{ + "https://cloud-dev.mongodbgov.com", + "https://cloud-qa.mongodbgov.com", } - - if data.SecretName.ValueString() == "" { - data.SecretName = types.StringValue(MultiEnvDefaultFunc([]string{ - "SECRET_NAME", - "TF_VAR_SECRET_NAME", - }, "").(string)) + if providerIsMongodbGovCloud && !slices.Contains(govAdditionalURLs, config.NormalizeBaseURL(providerBaseURL)) { + return govURL } - - return *data + return providerBaseURL } func (p *MongodbtlasProvider) DataSources(context.Context) []func() datasource.DataSource { @@ -442,26 +352,3 @@ func MuxProviderFactory() func() tfprotov6.ProviderServer { } return muxServer.ProviderServer } - -func MultiEnvDefaultFunc(ks []string, def any) any { - for _, k := range ks { - if v := os.Getenv(k); v != "" { - return v - } - } - return def -} - -func isGovBaseURLConfigured(baseURL string) bool { - if baseURL == "" { - baseURL = MultiEnvDefaultFunc([]string{ - "MONGODB_ATLAS_BASE_URL", - "MCLI_OPS_MANAGER_URL", - }, "").(string) - } - return baseURL == MongodbGovCloudDevURL || baseURL == MongodbGovCloudQAURL -} - -func isGovBaseURLConfiguredForProvider(data *tfMongodbAtlasProviderModel) bool { - return isGovBaseURLConfigured(data.BaseURL.ValueString()) -} diff --git a/internal/provider/provider_authentication_test.go b/internal/provider/provider_authentication_test.go index d1b0cfc90e..202d8a1baa 100644 --- a/internal/provider/provider_authentication_test.go +++ b/internal/provider/provider_authentication_test.go @@ -63,7 +63,6 @@ func TestAccServiceAccount_basic(t *testing.T) { } func TestAccAccessToken_basic(t *testing.T) { - acc.SkipTestForCI(t) // access token has a validity period of 1 hour, so it cannot be used in CI reliably acc.SkipInPAK(t, "skipping as this test is for Token credentials only") acc.SkipInSA(t, "skipping as this test is for Token credentials only") var ( diff --git a/internal/provider/provider_sdk2.go b/internal/provider/provider_sdk2.go index ea142c81c3..1ae6937211 100644 --- a/internal/provider/provider_sdk2.go +++ b/internal/provider/provider_sdk2.go @@ -2,11 +2,11 @@ package provider import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/accesslistapikey" "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/apikey" @@ -52,11 +52,6 @@ import ( "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/x509authenticationdatabaseuser" ) -type SecretData struct { - PublicKey string `json:"public_key"` - PrivateKey string `json:"private_key"` -} - // NewSdkV2Provider returns the provider to be use by the code. func NewSdkV2Provider() *schema.Provider { provider := &schema.Provider{ @@ -118,6 +113,21 @@ func NewSdkV2Provider() *schema.Provider { Optional: true, Description: "AWS Security Token Service provided session token.", }, + "client_id": { + Type: schema.TypeString, + Optional: true, + Description: "MongoDB Atlas Client ID for Service Account.", + }, + "client_secret": { + Type: schema.TypeString, + Optional: true, + Description: "MongoDB Atlas Client Secret for Service Account.", + }, + "access_token": { + Type: schema.TypeString, + Optional: true, + Description: "MongoDB Atlas Access Token for Service Account.", + }, }, DataSourcesMap: getDataSourcesMap(), ResourcesMap: getResourcesMap(), @@ -144,6 +154,23 @@ func NewSdkV2Provider() *schema.Provider { return provider } +func assumeRoleSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "role_arn": { + Type: schema.TypeString, + Optional: true, + Description: "Amazon Resource Name (ARN) of an IAM Role to assume prior to making API calls.", + }, + }, + }, + } +} + func getDataSourcesMap() map[string]*schema.Resource { dataSourcesMap := map[string]*schema.Resource{ "mongodbatlas_custom_db_role": customdbrole.DataSource(), @@ -268,190 +295,47 @@ func getResourcesMap() map[string]*schema.Resource { func providerConfigure(provider *schema.Provider) func(ctx context.Context, d *schema.ResourceData) (any, diag.Diagnostics) { return func(ctx context.Context, d *schema.ResourceData) (any, diag.Diagnostics) { - diagnostics := setDefaultsAndValidations(d) - if diagnostics.HasError() { - return nil, diagnostics + var diags diag.Diagnostics + providerVars := getSDKv2ProviderVars(d) + c, err := config.GetCredentials(providerVars, config.NewEnvVars(), getAWSCredentials) + if err != nil { + return nil, append(diags, diag.FromErr(fmt.Errorf("error getting credentials for provider: %w", err))...) } - - cfg := config.Config{ - PublicKey: d.Get("public_key").(string), - PrivateKey: d.Get("private_key").(string), - BaseURL: d.Get("base_url").(string), - RealmBaseURL: d.Get("realm_base_url").(string), - TerraformVersion: provider.TerraformVersion, + // Don't log possible warnings or errors as they will be logged by the TPF provider. + if c.Errors() != "" { + return nil, nil } - - assumeRoleValue, ok := d.GetOk("assume_role") - awsRoleDefined := ok && len(assumeRoleValue.([]any)) > 0 && assumeRoleValue.([]any)[0] != nil - if awsRoleDefined { - cfg.AssumeRoleARN = getAssumeRoleARN(assumeRoleValue.([]any)[0].(map[string]any)) - secret := d.Get("secret_name").(string) - region := conversion.MongoDBRegionToAWSRegion(d.Get("region").(string)) - awsAccessKeyID := d.Get("aws_access_key_id").(string) - awsSecretAccessKey := d.Get("aws_secret_access_key").(string) - awsSessionToken := d.Get("aws_session_token").(string) - endpoint := d.Get("sts_endpoint").(string) - var err error - cfg, err = configureCredentialsSTS(&cfg, secret, region, awsAccessKeyID, awsSecretAccessKey, awsSessionToken, endpoint) - if err != nil { - return nil, append(diagnostics, diag.FromErr(err)...) - } - } - - client, err := cfg.NewClient(ctx) + client, err := config.NewClient(c, provider.TerraformVersion) if err != nil { - return nil, append(diagnostics, diag.FromErr(err)...) + return nil, append(diags, diag.FromErr(fmt.Errorf("error initializing provider: %w", err))...) } - return client, diagnostics + return client, nil } } -func setDefaultsAndValidations(d *schema.ResourceData) diag.Diagnostics { - diagnostics := []diag.Diagnostic{} - - mongodbgovCloud := conversion.Pointer(d.Get("is_mongodbgov_cloud").(bool)) - if *mongodbgovCloud { - if !isGovBaseURLConfiguredForSDK2Provider(d) { - if err := d.Set("base_url", MongodbGovCloudURL); err != nil { - return append(diagnostics, diag.FromErr(err)...) - } - } - } - - if err := setValueFromConfigOrEnv(d, "base_url", []string{ - "MONGODB_ATLAS_BASE_URL", - "MCLI_OPS_MANAGER_URL", - }); err != nil { - return append(diagnostics, diag.FromErr(err)...) - } - - awsRoleDefined := false +func getSDKv2ProviderVars(d *schema.ResourceData) *config.Vars { + assumeRoleARN := "" assumeRoles := d.Get("assume_role").([]any) - if len(assumeRoles) == 0 { - roleArn := MultiEnvDefaultFunc([]string{ - "ASSUME_ROLE_ARN", - "TF_VAR_ASSUME_ROLE_ARN", - }, "").(string) - if roleArn != "" { - awsRoleDefined = true - if err := d.Set("assume_role", []map[string]any{{"role_arn": roleArn}}); err != nil { - return append(diagnostics, diag.FromErr(err)...) - } + if len(assumeRoles) > 0 { + if assumeRole, ok := assumeRoles[0].(map[string]any); ok { + assumeRoleARN = assumeRole["role_arn"].(string) } - } else { - awsRoleDefined = true - } - - if err := setValueFromConfigOrEnv(d, "public_key", []string{ - "MONGODB_ATLAS_PUBLIC_API_KEY", - "MONGODB_ATLAS_PUBLIC_KEY", - "MCLI_PUBLIC_API_KEY", - }); err != nil { - return append(diagnostics, diag.FromErr(err)...) - } - if d.Get("public_key").(string) == "" && !awsRoleDefined { - diagnostics = append(diagnostics, diag.Diagnostic{Severity: diag.Warning, Summary: MissingAuthAttrError}) - } - - if err := setValueFromConfigOrEnv(d, "private_key", []string{ - "MONGODB_ATLAS_PRIVATE_API_KEY", - "MONGODB_ATLAS_PRIVATE_KEY", - "MCLI_PRIVATE_API_KEY", - }); err != nil { - return append(diagnostics, diag.FromErr(err)...) - } - - if d.Get("private_key").(string) == "" && !awsRoleDefined { - diagnostics = append(diagnostics, diag.Diagnostic{Severity: diag.Warning, Summary: MissingAuthAttrError}) - } - - if err := setValueFromConfigOrEnv(d, "realm_base_url", []string{ - "MONGODB_REALM_BASE_URL", - }); err != nil { - return append(diagnostics, diag.FromErr(err)...) - } - - if err := setValueFromConfigOrEnv(d, "region", []string{ - "AWS_REGION", - "TF_VAR_AWS_REGION", - }); err != nil { - return append(diagnostics, diag.FromErr(err)...) - } - - if err := setValueFromConfigOrEnv(d, "sts_endpoint", []string{ - "STS_ENDPOINT", - "TF_VAR_STS_ENDPOINT", - }); err != nil { - return append(diagnostics, diag.FromErr(err)...) - } - - if err := setValueFromConfigOrEnv(d, "aws_access_key_id", []string{ - "AWS_ACCESS_KEY_ID", - "TF_VAR_AWS_ACCESS_KEY_ID", - }); err != nil { - return append(diagnostics, diag.FromErr(err)...) } - - if err := setValueFromConfigOrEnv(d, "aws_secret_access_key", []string{ - "AWS_SECRET_ACCESS_KEY", - "TF_VAR_AWS_SECRET_ACCESS_KEY", - }); err != nil { - return append(diagnostics, diag.FromErr(err)...) - } - - if err := setValueFromConfigOrEnv(d, "secret_name", []string{ - "SECRET_NAME", - "TF_VAR_SECRET_NAME", - }); err != nil { - return append(diagnostics, diag.FromErr(err)...) + baseURL := applyGovBaseURLIfNeeded(d.Get("base_url").(string), d.Get("is_mongodbgov_cloud").(bool)) + return &config.Vars{ + AccessToken: d.Get("access_token").(string), + ClientID: d.Get("client_id").(string), + ClientSecret: d.Get("client_secret").(string), + PublicKey: d.Get("public_key").(string), + PrivateKey: d.Get("private_key").(string), + BaseURL: baseURL, + RealmBaseURL: d.Get("realm_base_url").(string), + AWSAssumeRoleARN: assumeRoleARN, + AWSSecretName: d.Get("secret_name").(string), + AWSRegion: d.Get("region").(string), + AWSAccessKeyID: d.Get("aws_access_key_id").(string), + AWSSecretAccessKey: d.Get("aws_secret_access_key").(string), + AWSSessionToken: d.Get("aws_session_token").(string), + AWSEndpoint: d.Get("sts_endpoint").(string), } - - if err := setValueFromConfigOrEnv(d, "aws_session_token", []string{ - "AWS_SESSION_TOKEN", - "TF_VAR_AWS_SESSION_TOKEN", - }); err != nil { - return append(diagnostics, diag.FromErr(err)...) - } - - return diagnostics -} - -func setValueFromConfigOrEnv(d *schema.ResourceData, attrName string, envVars []string) error { - var val = d.Get(attrName).(string) - if val == "" { - val = MultiEnvDefaultFunc(envVars, "").(string) - } - return d.Set(attrName, val) -} - -// assumeRoleSchema From aws provider.go -func assumeRoleSchema() *schema.Schema { - return &schema.Schema{ - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "role_arn": { - Type: schema.TypeString, - Optional: true, - Description: "Amazon Resource Name (ARN) of an IAM Role to assume prior to making API calls.", - }, - }, - }, - } -} - -func getAssumeRoleARN(tfMap map[string]any) string { - if tfMap == nil { - return "" - } - if v, ok := tfMap["role_arn"].(string); ok && v != "" { - return v - } - return "" -} - -func isGovBaseURLConfiguredForSDK2Provider(d *schema.ResourceData) bool { - return isGovBaseURLConfigured(d.Get("base_url").(string)) } diff --git a/internal/service/advancedcluster/common.go b/internal/service/advancedcluster/common.go index 1160f05baa..baa962bcd3 100644 --- a/internal/service/advancedcluster/common.go +++ b/internal/service/advancedcluster/common.go @@ -6,7 +6,6 @@ import ( "strings" "time" - admin20240530 "go.mongodb.org/atlas-sdk/v20240530005/admin" "go.mongodb.org/atlas-sdk/v20250312008/admin" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -27,7 +26,6 @@ var ( ) type ProcessArgs struct { - ArgsLegacy *admin20240530.ClusterDescriptionProcessArgs ArgsDefault *admin.ClusterDescriptionProcessArgs20240805 ClusterAdvancedConfig *admin.ApiAtlasClusterAdvancedConfiguration } diff --git a/internal/service/advancedcluster/common_model_sdk_version_conversion.go b/internal/service/advancedcluster/common_model_sdk_version_conversion.go deleted file mode 100644 index 1f8f2b5afd..0000000000 --- a/internal/service/advancedcluster/common_model_sdk_version_conversion.go +++ /dev/null @@ -1,256 +0,0 @@ -package advancedcluster - -import ( - admin20240530 "go.mongodb.org/atlas-sdk/v20240530005/admin" - admin20240805 "go.mongodb.org/atlas-sdk/v20240805005/admin" - "go.mongodb.org/atlas-sdk/v20250312008/admin" -) - -// Conversions from one SDK model version to another are used to avoid duplicating our flatten/expand conversion functions. -// - These functions must not contain any business logic. -// - All will be removed once we rely on a single API version. - -func ConvertClusterDescription20241023to20240805(clusterDescription *admin.ClusterDescription20240805) *admin20240805.ClusterDescription20240805 { - return &admin20240805.ClusterDescription20240805{ - Name: clusterDescription.Name, - ClusterType: clusterDescription.ClusterType, - ReplicationSpecs: convertReplicationSpecs20241023to20240805(clusterDescription.ReplicationSpecs), - BackupEnabled: clusterDescription.BackupEnabled, - BiConnector: convertBiConnector20241023to20240805(clusterDescription.BiConnector), - EncryptionAtRestProvider: clusterDescription.EncryptionAtRestProvider, - Labels: convertLabels20241023to20240805(clusterDescription.Labels), - Tags: convertTag20241023to20240805(clusterDescription.Tags), - MongoDBMajorVersion: clusterDescription.MongoDBMajorVersion, - PitEnabled: clusterDescription.PitEnabled, - RootCertType: clusterDescription.RootCertType, - TerminationProtectionEnabled: clusterDescription.TerminationProtectionEnabled, - VersionReleaseSystem: clusterDescription.VersionReleaseSystem, - GlobalClusterSelfManagedSharding: clusterDescription.GlobalClusterSelfManagedSharding, - ReplicaSetScalingStrategy: clusterDescription.ReplicaSetScalingStrategy, - RedactClientLogData: clusterDescription.RedactClientLogData, - ConfigServerManagementMode: clusterDescription.ConfigServerManagementMode, - AdvancedConfiguration: convertAdvancedConfiguration20250312to20240805(clusterDescription.AdvancedConfiguration), - } -} - -func convertReplicationSpecs20241023to20240805(replicationSpecs *[]admin.ReplicationSpec20240805) *[]admin20240805.ReplicationSpec20240805 { - if replicationSpecs == nil { - return nil - } - result := make([]admin20240805.ReplicationSpec20240805, len(*replicationSpecs)) - for i, replicationSpec := range *replicationSpecs { - result[i] = admin20240805.ReplicationSpec20240805{ - Id: replicationSpec.Id, - ZoneName: replicationSpec.ZoneName, - ZoneId: replicationSpec.ZoneId, - RegionConfigs: convertCloudRegionConfig20241023to20240805(replicationSpec.RegionConfigs), - } - } - return &result -} - -func convertCloudRegionConfig20241023to20240805(cloudRegionConfig *[]admin.CloudRegionConfig20240805) *[]admin20240805.CloudRegionConfig20240805 { - if cloudRegionConfig == nil { - return nil - } - result := make([]admin20240805.CloudRegionConfig20240805, len(*cloudRegionConfig)) - for i, regionConfig := range *cloudRegionConfig { - result[i] = admin20240805.CloudRegionConfig20240805{ - ProviderName: regionConfig.ProviderName, - RegionName: regionConfig.RegionName, - BackingProviderName: regionConfig.BackingProviderName, - Priority: regionConfig.Priority, - ElectableSpecs: convertHardwareSpec20241023to20240805(regionConfig.ElectableSpecs), - ReadOnlySpecs: convertDedicatedHardwareSpec20241023to20240805(regionConfig.ReadOnlySpecs), - AnalyticsSpecs: convertDedicatedHardwareSpec20241023to20240805(regionConfig.AnalyticsSpecs), - AutoScaling: convertAdvancedAutoScalingSettings20241023to20240805(regionConfig.AutoScaling), - AnalyticsAutoScaling: convertAdvancedAutoScalingSettings20241023to20240805(regionConfig.AnalyticsAutoScaling), - } - } - return &result -} - -func convertAdvancedAutoScalingSettings20241023to20240805(advancedAutoScalingSettings *admin.AdvancedAutoScalingSettings) *admin20240805.AdvancedAutoScalingSettings { - if advancedAutoScalingSettings == nil { - return nil - } - return &admin20240805.AdvancedAutoScalingSettings{ - Compute: convertAdvancedComputeAutoScaling20241023to20240805(advancedAutoScalingSettings.Compute), - DiskGB: convertDiskGBAutoScaling20241023to20240805(advancedAutoScalingSettings.DiskGB), - } -} - -func convertDiskGBAutoScaling20241023to20240805(diskGBAutoScaling *admin.DiskGBAutoScaling) *admin20240805.DiskGBAutoScaling { - if diskGBAutoScaling == nil { - return nil - } - return &admin20240805.DiskGBAutoScaling{ - Enabled: diskGBAutoScaling.Enabled, - } -} - -func convertAdvancedComputeAutoScaling20241023to20240805(advancedComputeAutoScaling *admin.AdvancedComputeAutoScaling) *admin20240805.AdvancedComputeAutoScaling { - if advancedComputeAutoScaling == nil { - return nil - } - return &admin20240805.AdvancedComputeAutoScaling{ - Enabled: advancedComputeAutoScaling.Enabled, - MaxInstanceSize: advancedComputeAutoScaling.MaxInstanceSize, - MinInstanceSize: advancedComputeAutoScaling.MinInstanceSize, - ScaleDownEnabled: advancedComputeAutoScaling.ScaleDownEnabled, - } -} - -func convertHardwareSpec20241023to20240805(hardwareSpec *admin.HardwareSpec20240805) *admin20240805.HardwareSpec20240805 { - if hardwareSpec == nil { - return nil - } - return &admin20240805.HardwareSpec20240805{ - DiskSizeGB: hardwareSpec.DiskSizeGB, - NodeCount: hardwareSpec.NodeCount, - DiskIOPS: hardwareSpec.DiskIOPS, - EbsVolumeType: hardwareSpec.EbsVolumeType, - InstanceSize: hardwareSpec.InstanceSize, - } -} - -func convertDedicatedHardwareSpec20241023to20240805(hardwareSpec *admin.DedicatedHardwareSpec20240805) *admin20240805.DedicatedHardwareSpec20240805 { - if hardwareSpec == nil { - return nil - } - return &admin20240805.DedicatedHardwareSpec20240805{ - DiskSizeGB: hardwareSpec.DiskSizeGB, - NodeCount: hardwareSpec.NodeCount, - DiskIOPS: hardwareSpec.DiskIOPS, - EbsVolumeType: hardwareSpec.EbsVolumeType, - InstanceSize: hardwareSpec.InstanceSize, - } -} - -func convertBiConnector20241023to20240805(biConnector *admin.BiConnector) *admin20240805.BiConnector { - if biConnector == nil { - return nil - } - return &admin20240805.BiConnector{ - ReadPreference: biConnector.ReadPreference, - Enabled: biConnector.Enabled, - } -} - -func convertAdvancedConfiguration20250312to20240805(advConfig *admin.ApiAtlasClusterAdvancedConfiguration) *admin20240805.ApiAtlasClusterAdvancedConfiguration { - if advConfig == nil { - return nil - } - - return &admin20240805.ApiAtlasClusterAdvancedConfiguration{ - MinimumEnabledTlsProtocol: advConfig.MinimumEnabledTlsProtocol, - CustomOpensslCipherConfigTls12: advConfig.CustomOpensslCipherConfigTls12, - TlsCipherConfigMode: advConfig.TlsCipherConfigMode, - } -} - -func convertLabels20241023to20240805(labels *[]admin.ComponentLabel) *[]admin20240805.ComponentLabel { - if labels == nil { - return nil - } - result := make([]admin20240805.ComponentLabel, len(*labels)) - for i, label := range *labels { - result[i] = admin20240805.ComponentLabel{ - Key: label.Key, - Value: label.Value, - } - } - return &result -} - -func convertTag20241023to20240805(tags *[]admin.ResourceTag) *[]admin20240805.ResourceTag { - if tags == nil { - return nil - } - result := make([]admin20240805.ResourceTag, len(*tags)) - for i, tag := range *tags { - result[i] = admin20240805.ResourceTag{ - Key: tag.Key, - Value: tag.Value, - } - } - return &result -} - -func ConvertRegionConfigSlice20241023to20240530(slice *[]admin.CloudRegionConfig20240805) *[]admin20240530.CloudRegionConfig { - if slice == nil { - return nil - } - cloudRegionSlice := *slice - results := make([]admin20240530.CloudRegionConfig, len(cloudRegionSlice)) - for i := range cloudRegionSlice { - cloudRegion := cloudRegionSlice[i] - results[i] = admin20240530.CloudRegionConfig{ - ElectableSpecs: convertHardwareSpec20241023to20240530(cloudRegion.ElectableSpecs), - Priority: cloudRegion.Priority, - ProviderName: cloudRegion.ProviderName, - RegionName: cloudRegion.RegionName, - AnalyticsAutoScaling: convertAdvancedAutoScalingSettings20241023to20240530(cloudRegion.AnalyticsAutoScaling), - AnalyticsSpecs: convertDedicatedHardwareSpec20241023to20240530(cloudRegion.AnalyticsSpecs), - AutoScaling: convertAdvancedAutoScalingSettings20241023to20240530(cloudRegion.AutoScaling), - ReadOnlySpecs: convertDedicatedHardwareSpec20241023to20240530(cloudRegion.ReadOnlySpecs), - BackingProviderName: cloudRegion.BackingProviderName, - } - } - return &results -} - -func convertHardwareSpec20241023to20240530(hwspec *admin.HardwareSpec20240805) *admin20240530.HardwareSpec { - if hwspec == nil { - return nil - } - return &admin20240530.HardwareSpec{ - DiskIOPS: hwspec.DiskIOPS, - EbsVolumeType: hwspec.EbsVolumeType, - InstanceSize: hwspec.InstanceSize, - NodeCount: hwspec.NodeCount, - } -} - -func convertAdvancedAutoScalingSettings20241023to20240530(settings *admin.AdvancedAutoScalingSettings) *admin20240530.AdvancedAutoScalingSettings { - if settings == nil { - return nil - } - return &admin20240530.AdvancedAutoScalingSettings{ - Compute: convertAdvancedComputeAutoScaling20241023to20240530(settings.Compute), - DiskGB: convertDiskGBAutoScaling20241023to20240530(settings.DiskGB), - } -} - -func convertAdvancedComputeAutoScaling20241023to20240530(settings *admin.AdvancedComputeAutoScaling) *admin20240530.AdvancedComputeAutoScaling { - if settings == nil { - return nil - } - return &admin20240530.AdvancedComputeAutoScaling{ - Enabled: settings.Enabled, - MaxInstanceSize: settings.MaxInstanceSize, - MinInstanceSize: settings.MinInstanceSize, - ScaleDownEnabled: settings.ScaleDownEnabled, - } -} - -func convertDiskGBAutoScaling20241023to20240530(settings *admin.DiskGBAutoScaling) *admin20240530.DiskGBAutoScaling { - if settings == nil { - return nil - } - return &admin20240530.DiskGBAutoScaling{ - Enabled: settings.Enabled, - } -} - -func convertDedicatedHardwareSpec20241023to20240530(spec *admin.DedicatedHardwareSpec20240805) *admin20240530.DedicatedHardwareSpec { - if spec == nil { - return nil - } - return &admin20240530.DedicatedHardwareSpec{ - NodeCount: spec.NodeCount, - DiskIOPS: spec.DiskIOPS, - EbsVolumeType: spec.EbsVolumeType, - InstanceSize: spec.InstanceSize, - } -} diff --git a/internal/service/advancedcluster/common_model_sdk_version_conversion_test.go b/internal/service/advancedcluster/common_model_sdk_version_conversion_test.go deleted file mode 100644 index 74a52424ff..0000000000 --- a/internal/service/advancedcluster/common_model_sdk_version_conversion_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package advancedcluster_test - -import ( - "testing" - - "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" - "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/advancedcluster" - "github.com/stretchr/testify/assert" - admin20240805 "go.mongodb.org/atlas-sdk/v20240805005/admin" - "go.mongodb.org/atlas-sdk/v20250312008/admin" -) - -func TestConvertClusterDescription20241023to20240805(t *testing.T) { - var ( - clusterName = "clusterName" - clusterType = "REPLICASET" - earProvider = "AWS" - booleanValue = true - mongoDBMajorVersion = "7.0" - rootCertType = "rootCertType" - replicaSetScalingStrategy = "WORKLOAD_TYPE" - configServerManagementMode = "ATLAS_MANAGED" - readPreference = "primary" - zoneName = "z1" - id = "id1" - regionConfigProvider = "AWS" - region = "EU_WEST_1" - priority = 7 - instanceSize = "M10" - nodeCount = 3 - diskSizeGB = 30.3 - ebsVolumeType = "STANDARD" - diskIOPS = 100 - ) - testCases := []struct { - input *admin.ClusterDescription20240805 - expectedOutput *admin20240805.ClusterDescription20240805 - name string - }{ - { - name: "Converts cluster description from 20241023 to 20240805", - input: &admin.ClusterDescription20240805{ - Name: conversion.StringPtr(clusterName), - ClusterType: conversion.StringPtr(clusterType), - ReplicationSpecs: &[]admin.ReplicationSpec20240805{ - { - Id: conversion.StringPtr(id), - ZoneName: conversion.StringPtr(zoneName), - RegionConfigs: &[]admin.CloudRegionConfig20240805{ - { - ProviderName: conversion.StringPtr(regionConfigProvider), - RegionName: conversion.StringPtr(region), - BackingProviderName: conversion.StringPtr(regionConfigProvider), - Priority: conversion.IntPtr(priority), - AnalyticsSpecs: &admin.DedicatedHardwareSpec20240805{ - InstanceSize: conversion.StringPtr(instanceSize), - NodeCount: conversion.IntPtr(nodeCount), - DiskSizeGB: conversion.Pointer(diskSizeGB), - EbsVolumeType: conversion.StringPtr(ebsVolumeType), - DiskIOPS: conversion.IntPtr(diskIOPS), - }, - ElectableSpecs: &admin.HardwareSpec20240805{ - InstanceSize: conversion.StringPtr(instanceSize), - NodeCount: conversion.IntPtr(nodeCount), - DiskSizeGB: conversion.Pointer(diskSizeGB), - EbsVolumeType: conversion.StringPtr(ebsVolumeType), - DiskIOPS: conversion.IntPtr(diskIOPS), - }, - AutoScaling: &admin.AdvancedAutoScalingSettings{ - Compute: &admin.AdvancedComputeAutoScaling{ - Enabled: conversion.Pointer(booleanValue), - MaxInstanceSize: conversion.Pointer(instanceSize), - MinInstanceSize: conversion.Pointer(instanceSize), - ScaleDownEnabled: conversion.Pointer(booleanValue), - }, - DiskGB: &admin.DiskGBAutoScaling{ - Enabled: conversion.Pointer(booleanValue), - }, - }, - }, - }, - }, - }, - BackupEnabled: conversion.Pointer(booleanValue), - BiConnector: &admin.BiConnector{ - Enabled: conversion.Pointer(booleanValue), - ReadPreference: conversion.StringPtr(readPreference), - }, - EncryptionAtRestProvider: conversion.StringPtr(earProvider), - Labels: &[]admin.ComponentLabel{ - {Key: conversion.StringPtr("key1"), Value: conversion.StringPtr("value1")}, - {Key: conversion.StringPtr("key2"), Value: conversion.StringPtr("value2")}, - }, - Tags: &[]admin.ResourceTag{ - {Key: "key1", Value: "value1"}, - {Key: "key2", Value: "value2"}, - }, - MongoDBMajorVersion: conversion.StringPtr(mongoDBMajorVersion), - PitEnabled: conversion.Pointer(booleanValue), - RootCertType: conversion.StringPtr(rootCertType), - TerminationProtectionEnabled: conversion.Pointer(booleanValue), - VersionReleaseSystem: conversion.StringPtr(""), - GlobalClusterSelfManagedSharding: conversion.Pointer(booleanValue), - ReplicaSetScalingStrategy: conversion.StringPtr(replicaSetScalingStrategy), - RedactClientLogData: conversion.Pointer(booleanValue), - ConfigServerManagementMode: conversion.StringPtr(configServerManagementMode), - }, - expectedOutput: &admin20240805.ClusterDescription20240805{ - Name: conversion.StringPtr(clusterName), - ClusterType: conversion.StringPtr(clusterType), - ReplicationSpecs: &[]admin20240805.ReplicationSpec20240805{ - { - Id: conversion.StringPtr(id), - ZoneName: conversion.StringPtr(zoneName), - RegionConfigs: &[]admin20240805.CloudRegionConfig20240805{ - { - ProviderName: conversion.StringPtr(regionConfigProvider), - RegionName: conversion.StringPtr(region), - BackingProviderName: conversion.StringPtr(regionConfigProvider), - Priority: conversion.IntPtr(priority), - AnalyticsSpecs: &admin20240805.DedicatedHardwareSpec20240805{ - InstanceSize: conversion.StringPtr(instanceSize), - NodeCount: conversion.IntPtr(nodeCount), - DiskSizeGB: conversion.Pointer(diskSizeGB), - EbsVolumeType: conversion.StringPtr(ebsVolumeType), - DiskIOPS: conversion.IntPtr(diskIOPS), - }, - ElectableSpecs: &admin20240805.HardwareSpec20240805{ - InstanceSize: conversion.StringPtr(instanceSize), - NodeCount: conversion.IntPtr(nodeCount), - DiskSizeGB: conversion.Pointer(diskSizeGB), - EbsVolumeType: conversion.StringPtr(ebsVolumeType), - DiskIOPS: conversion.IntPtr(diskIOPS), - }, - AutoScaling: &admin20240805.AdvancedAutoScalingSettings{ - Compute: &admin20240805.AdvancedComputeAutoScaling{ - Enabled: conversion.Pointer(booleanValue), - MaxInstanceSize: conversion.Pointer(instanceSize), - MinInstanceSize: conversion.Pointer(instanceSize), - ScaleDownEnabled: conversion.Pointer(booleanValue), - }, - DiskGB: &admin20240805.DiskGBAutoScaling{ - Enabled: conversion.Pointer(booleanValue), - }, - }, - }, - }, - }, - }, - BackupEnabled: conversion.Pointer(booleanValue), - BiConnector: &admin20240805.BiConnector{ - Enabled: conversion.Pointer(booleanValue), - ReadPreference: conversion.StringPtr(readPreference), - }, - EncryptionAtRestProvider: conversion.StringPtr(earProvider), - Labels: &[]admin20240805.ComponentLabel{ - {Key: conversion.StringPtr("key1"), Value: conversion.StringPtr("value1")}, - {Key: conversion.StringPtr("key2"), Value: conversion.StringPtr("value2")}, - }, - Tags: &[]admin20240805.ResourceTag{ - {Key: "key1", Value: "value1"}, - {Key: "key2", Value: "value2"}, - }, - MongoDBMajorVersion: conversion.StringPtr(mongoDBMajorVersion), - PitEnabled: conversion.Pointer(booleanValue), - RootCertType: conversion.StringPtr(rootCertType), - TerminationProtectionEnabled: conversion.Pointer(booleanValue), - VersionReleaseSystem: conversion.StringPtr(""), - GlobalClusterSelfManagedSharding: conversion.Pointer(booleanValue), - ReplicaSetScalingStrategy: conversion.StringPtr(replicaSetScalingStrategy), - RedactClientLogData: conversion.Pointer(booleanValue), - ConfigServerManagementMode: conversion.StringPtr(configServerManagementMode), - }, - }, - { - name: "Converts cluster description from 20241023 to 20240805 with nil values", - input: &admin.ClusterDescription20240805{}, - expectedOutput: &admin20240805.ClusterDescription20240805{ - ReplicationSpecs: nil, - BiConnector: nil, - Labels: nil, - Tags: nil, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result := advancedcluster.ConvertClusterDescription20241023to20240805(tc.input) - assert.Equal(t, tc.expectedOutput, result) - }) - } -} diff --git a/internal/service/advancedcluster/resource_test.go b/internal/service/advancedcluster/resource_test.go index de152464fd..8fed2f2153 100644 --- a/internal/service/advancedcluster/resource_test.go +++ b/internal/service/advancedcluster/resource_test.go @@ -9,7 +9,6 @@ import ( "testing" "time" - admin20240530 "go.mongodb.org/atlas-sdk/v20240530005/admin" "go.mongodb.org/atlas-sdk/v20250312008/admin" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -288,8 +287,7 @@ func TestAccClusterAdvancedCluster_pausedToUnpaused(t *testing.T) { func TestAccClusterAdvancedCluster_advancedConfig_oldMongoDBVersion(t *testing.T) { var ( projectID, clusterName = acc.ProjectIDExecutionWithCluster(t, 4) - - processArgs20240530 = &admin20240530.ClusterDescriptionProcessArgs{ + processArgsCommon = &admin.ClusterDescriptionProcessArgs20240805{ DefaultWriteConcern: conversion.StringPtr("1"), JavascriptEnabled: conversion.Pointer(true), MinimumEnabledTlsProtocol: conversion.StringPtr("TLS1_2"), @@ -299,16 +297,13 @@ func TestAccClusterAdvancedCluster_advancedConfig_oldMongoDBVersion(t *testing.T SampleSizeBIConnector: conversion.Pointer(110), TransactionLifetimeLimitSeconds: conversion.Pointer[int64](300), } - processArgs = &admin.ClusterDescriptionProcessArgs20240805{ - ChangeStreamOptionsPreAndPostImagesExpireAfterSeconds: conversion.IntPtr(-1), // this will not be set in the TF configuration - DefaultMaxTimeMS: conversion.IntPtr(65), - } - - processArgsCipherConfig = &admin.ClusterDescriptionProcessArgs20240805{ - TlsCipherConfigMode: conversion.StringPtr("CUSTOM"), - CustomOpensslCipherConfigTls12: &[]string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"}, - } ) + processArgs := *processArgsCommon + processArgs.DefaultMaxTimeMS = conversion.IntPtr(65) + + processArgsCipherConfig := *processArgsCommon + processArgsCipherConfig.TlsCipherConfigMode = conversion.StringPtr("CUSTOM") + processArgsCipherConfig.CustomOpensslCipherConfigTls12 = &[]string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"} resource.ParallelTest(t, resource.TestCase{ PreCheck: acc.PreCheckBasicSleep(t, nil, projectID, clusterName), @@ -316,12 +311,12 @@ func TestAccClusterAdvancedCluster_advancedConfig_oldMongoDBVersion(t *testing.T CheckDestroy: acc.CheckDestroyCluster, Steps: []resource.TestStep{ { - Config: configAdvanced(t, projectID, clusterName, "7.0", processArgs20240530, processArgs), + Config: configAdvanced(t, projectID, clusterName, "7.0", &processArgs), ExpectError: regexp.MustCompile(errDefaultMaxTimeMinVersion), }, { - Config: configAdvanced(t, projectID, clusterName, "7.0", processArgs20240530, processArgsCipherConfig), - Check: checkAdvanced(clusterName, "TLS1_2", processArgsCipherConfig), + Config: configAdvanced(t, projectID, clusterName, "7.0", &processArgsCipherConfig), + Check: checkAdvanced(clusterName, "TLS1_2", &processArgsCipherConfig), }, acc.TestStepImportCluster(resourceName), }, @@ -332,7 +327,7 @@ func TestAccClusterAdvancedCluster_advancedConfig(t *testing.T) { var ( projectID, clusterName = acc.ProjectIDExecutionWithCluster(t, 4) clusterNameUpdated = acc.RandomClusterName() - processArgs20240530 = &admin20240530.ClusterDescriptionProcessArgs{ + processArgs = &admin.ClusterDescriptionProcessArgs20240805{ DefaultWriteConcern: conversion.StringPtr("1"), JavascriptEnabled: conversion.Pointer(true), MinimumEnabledTlsProtocol: conversion.StringPtr("TLS1_2"), @@ -341,13 +336,10 @@ func TestAccClusterAdvancedCluster_advancedConfig(t *testing.T) { SampleRefreshIntervalBIConnector: conversion.Pointer(310), SampleSizeBIConnector: conversion.Pointer(110), TransactionLifetimeLimitSeconds: conversion.Pointer[int64](300), - } - processArgs = &admin.ClusterDescriptionProcessArgs20240805{ ChangeStreamOptionsPreAndPostImagesExpireAfterSeconds: conversion.IntPtr(-1), // this will not be set in the TF configuration TlsCipherConfigMode: conversion.StringPtr("DEFAULT"), } - - processArgs20240530Updated = &admin20240530.ClusterDescriptionProcessArgs{ + processArgsUpdated = &admin.ClusterDescriptionProcessArgs20240805{ DefaultWriteConcern: conversion.StringPtr("0"), JavascriptEnabled: conversion.Pointer(true), MinimumEnabledTlsProtocol: conversion.StringPtr("TLS1_2"), @@ -356,9 +348,7 @@ func TestAccClusterAdvancedCluster_advancedConfig(t *testing.T) { SampleRefreshIntervalBIConnector: conversion.Pointer(310), SampleSizeBIConnector: conversion.Pointer(110), TransactionLifetimeLimitSeconds: conversion.Pointer[int64](300), - } - processArgsUpdated = &admin.ClusterDescriptionProcessArgs20240805{ - DefaultMaxTimeMS: conversion.IntPtr(65), + DefaultMaxTimeMS: conversion.IntPtr(65), ChangeStreamOptionsPreAndPostImagesExpireAfterSeconds: conversion.IntPtr(100), TlsCipherConfigMode: conversion.StringPtr("CUSTOM"), CustomOpensslCipherConfigTls12: &[]string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"}, @@ -376,15 +366,15 @@ func TestAccClusterAdvancedCluster_advancedConfig(t *testing.T) { CheckDestroy: acc.CheckDestroyCluster, Steps: []resource.TestStep{ { - Config: configAdvanced(t, projectID, clusterName, "", processArgs20240530, processArgs), + Config: configAdvanced(t, projectID, clusterName, "", processArgs), Check: checkAdvanced(clusterName, "TLS1_2", processArgs), }, { - Config: configAdvanced(t, projectID, clusterNameUpdated, "", processArgs20240530Updated, processArgsUpdated), + Config: configAdvanced(t, projectID, clusterNameUpdated, "", processArgsUpdated), Check: checkAdvanced(clusterNameUpdated, "TLS1_2", processArgsUpdated), }, { - Config: configAdvanced(t, projectID, clusterNameUpdated, "", processArgs20240530Updated, processArgsUpdatedCipherConfig), + Config: configAdvanced(t, projectID, clusterNameUpdated, "", processArgsUpdatedCipherConfig), Check: checkAdvanced(clusterNameUpdated, "TLS1_2", processArgsUpdatedCipherConfig), }, acc.TestStepImportCluster(resourceName), @@ -396,7 +386,7 @@ func TestAccClusterAdvancedCluster_defaultWrite(t *testing.T) { var ( projectID, clusterName = acc.ProjectIDExecutionWithCluster(t, 4) clusterNameUpdated = acc.RandomClusterName() - processArgs = &admin20240530.ClusterDescriptionProcessArgs{ + processArgs = &admin.ClusterDescriptionProcessArgs20240805{ DefaultWriteConcern: conversion.StringPtr("1"), JavascriptEnabled: conversion.Pointer(true), MinimumEnabledTlsProtocol: conversion.StringPtr("TLS1_2"), @@ -405,7 +395,7 @@ func TestAccClusterAdvancedCluster_defaultWrite(t *testing.T) { SampleRefreshIntervalBIConnector: conversion.Pointer(310), SampleSizeBIConnector: conversion.Pointer(110), } - processArgsUpdated = &admin20240530.ClusterDescriptionProcessArgs{ + processArgsUpdated = &admin.ClusterDescriptionProcessArgs20240805{ DefaultWriteConcern: conversion.StringPtr("majority"), JavascriptEnabled: conversion.Pointer(true), MinimumEnabledTlsProtocol: conversion.StringPtr("TLS1_2"), @@ -1118,7 +1108,7 @@ func TestAccAdvancedCluster_createTimeoutWithDeleteOnCreateReplicaset(t *testing Timeout: 60 * time.Second, IsDelete: true, }, "waiting for cluster to be deleted after cleanup in create timeout", diags) - time.Sleep(1 * time.Minute) // decrease the chance of `CONTAINER_WAITING_FOR_FAST_RECORD_CLEAN_UP`: "A transient error occurred. Please try again in a minute or use a different name" + time.Sleep(2 * time.Minute) // decrease the chance of `CONTAINER_WAITING_FOR_FAST_RECORD_CLEAN_UP`: "A transient error occurred. Please try again in a minute or use a different name" } ) resource.ParallelTest(t, *createCleanupTest(t, configCall, waitOnClusterDeleteDone, true)) @@ -1903,33 +1893,45 @@ func checkSingleProviderPaused(name string, paused bool) resource.TestCheckFunc "paused": strconv.FormatBool(paused)}) } -func configAdvanced(t *testing.T, projectID, clusterName, mongoDBMajorVersion string, p20240530 *admin20240530.ClusterDescriptionProcessArgs, p *admin.ClusterDescriptionProcessArgs20240805) string { +func configAdvanced(t *testing.T, projectID, clusterName, mongoDBMajorVersion string, p *admin.ClusterDescriptionProcessArgs20240805) string { t.Helper() - changeStreamOptionsStr := "" - defaultMaxTimeStr := "" - tlsCipherConfigModeStr := "" - customOpensslCipherConfigTLS12Str := "" + advancedConfig := "" mongoDBMajorVersionStr := "" - - if p != nil { - if p.ChangeStreamOptionsPreAndPostImagesExpireAfterSeconds != nil && p.ChangeStreamOptionsPreAndPostImagesExpireAfterSeconds != conversion.IntPtr(-1) { - changeStreamOptionsStr = fmt.Sprintf(`change_stream_options_pre_and_post_images_expire_after_seconds = %[1]d`, *p.ChangeStreamOptionsPreAndPostImagesExpireAfterSeconds) - } - if p.DefaultMaxTimeMS != nil { - defaultMaxTimeStr = fmt.Sprintf(`default_max_time_ms = %[1]d`, *p.DefaultMaxTimeMS) - } - if p.TlsCipherConfigMode != nil { - tlsCipherConfigModeStr = fmt.Sprintf(`tls_cipher_config_mode = %[1]q`, *p.TlsCipherConfigMode) - if p.CustomOpensslCipherConfigTls12 != nil && len(*p.CustomOpensslCipherConfigTls12) > 0 { - customOpensslCipherConfigTLS12Str = fmt.Sprintf( - `custom_openssl_cipher_config_tls12 = [%s]`, - acc.JoinQuotedStrings(*p.CustomOpensslCipherConfigTls12), - ) - } + if mongoDBMajorVersion != "" { + mongoDBMajorVersionStr = fmt.Sprintf("mongo_db_major_version = %[1]q\n", mongoDBMajorVersion) + } + if p.JavascriptEnabled != nil { + advancedConfig += fmt.Sprintf("javascript_enabled = %[1]t\n", *p.JavascriptEnabled) + } + if p.NoTableScan != nil { + advancedConfig += fmt.Sprintf("no_table_scan = %[1]t\n", *p.NoTableScan) + } + if p.OplogSizeMB != nil { + advancedConfig += fmt.Sprintf("oplog_size_mb = %[1]d\n", *p.OplogSizeMB) + } + if p.SampleRefreshIntervalBIConnector != nil { + advancedConfig += fmt.Sprintf("sample_refresh_interval_bi_connector = %[1]d\n", *p.SampleRefreshIntervalBIConnector) + } + if p.SampleSizeBIConnector != nil { + advancedConfig += fmt.Sprintf("sample_size_bi_connector = %[1]d\n", *p.SampleSizeBIConnector) + } + if p.TransactionLifetimeLimitSeconds != nil { + advancedConfig += fmt.Sprintf("transaction_lifetime_limit_seconds = %[1]d\n", *p.TransactionLifetimeLimitSeconds) + } + if p.ChangeStreamOptionsPreAndPostImagesExpireAfterSeconds != nil && *p.ChangeStreamOptionsPreAndPostImagesExpireAfterSeconds != -1 { + advancedConfig += fmt.Sprintf("change_stream_options_pre_and_post_images_expire_after_seconds = %[1]d\n", *p.ChangeStreamOptionsPreAndPostImagesExpireAfterSeconds) + } + if p.DefaultMaxTimeMS != nil { + advancedConfig += fmt.Sprintf("default_max_time_ms = %[1]d\n", *p.DefaultMaxTimeMS) + } + if p.TlsCipherConfigMode != nil { + advancedConfig += fmt.Sprintf("tls_cipher_config_mode = %[1]q\n", *p.TlsCipherConfigMode) + if p.CustomOpensslCipherConfigTls12 != nil && len(*p.CustomOpensslCipherConfigTls12) > 0 { + advancedConfig += fmt.Sprintf("custom_openssl_cipher_config_tls12 = [%s]\n", acc.JoinQuotedStrings(*p.CustomOpensslCipherConfigTls12)) } } - if mongoDBMajorVersion != "" { - mongoDBMajorVersionStr = fmt.Sprintf(`mongo_db_major_version = %[1]q`, mongoDBMajorVersion) + if p.MinimumEnabledTlsProtocol != nil { + advancedConfig += fmt.Sprintf("minimum_enabled_tls_protocol = %[1]q\n", *p.MinimumEnabledTlsProtocol) } return fmt.Sprintf(` @@ -1937,8 +1939,7 @@ func configAdvanced(t *testing.T, projectID, clusterName, mongoDBMajorVersion st project_id = %[1]q name = %[2]q cluster_type = "REPLICASET" - %[12]s - + %[3]s replication_specs = [{ region_configs = [{ electable_specs = { @@ -1956,22 +1957,10 @@ func configAdvanced(t *testing.T, projectID, clusterName, mongoDBMajorVersion st }] advanced_configuration = { - javascript_enabled = %[3]t - minimum_enabled_tls_protocol = %[4]q - no_table_scan = %[5]t - oplog_size_mb = %[6]d - sample_size_bi_connector = %[7]d - sample_refresh_interval_bi_connector = %[8]d - transaction_lifetime_limit_seconds = %[9]d - %[10]s - %[11]s - %[13]s - %[14]s + %[4]s } } - `, projectID, clusterName, p20240530.GetJavascriptEnabled(), p20240530.GetMinimumEnabledTlsProtocol(), p20240530.GetNoTableScan(), - p20240530.GetOplogSizeMB(), p20240530.GetSampleSizeBIConnector(), p20240530.GetSampleRefreshIntervalBIConnector(), p20240530.GetTransactionLifetimeLimitSeconds(), - changeStreamOptionsStr, defaultMaxTimeStr, mongoDBMajorVersionStr, tlsCipherConfigModeStr, customOpensslCipherConfigTLS12Str) + dataSourcesConfig + `, projectID, clusterName, mongoDBMajorVersionStr, advancedConfig) + dataSourcesConfig } func checkAdvanced(name, tls string, processArgs *admin.ClusterDescriptionProcessArgs20240805) resource.TestCheckFunc { @@ -2013,7 +2002,7 @@ func checkAdvanced(name, tls string, processArgs *admin.ClusterDescriptionProces ) } -func configAdvancedDefaultWrite(t *testing.T, projectID, clusterName string, p *admin20240530.ClusterDescriptionProcessArgs) string { +func configAdvancedDefaultWrite(t *testing.T, projectID, clusterName string, p *admin.ClusterDescriptionProcessArgs20240805) string { t.Helper() return fmt.Sprintf(` resource "mongodbatlas_advanced_cluster" "test" { @@ -2699,13 +2688,13 @@ func configPriority(t *testing.T, projectID, clusterName string, swapPriorities func configBiConnectorConfig(t *testing.T, projectID, name string, enabled bool) string { t.Helper() - additionalConfig := ` + advancedConfig := ` bi_connector_config = { enabled = false } ` if enabled { - additionalConfig = ` + advancedConfig = ` bi_connector_config = { enabled = true read_preference = "secondary" @@ -2737,7 +2726,7 @@ func configBiConnectorConfig(t *testing.T, projectID, name string, enabled bool) %[3]s } - `, projectID, name, additionalConfig) + dataSourcesConfig + `, projectID, name, advancedConfig) + dataSourcesConfig } func checkTenantBiConnectorConfig(projectID, name string, enabled bool) resource.TestCheckFunc { diff --git a/internal/service/cloudbackupschedule/resource_cloud_backup_schedule_migration_test.go b/internal/service/cloudbackupschedule/resource_cloud_backup_schedule_migration_test.go index 8475d75015..eeb0caff4f 100644 --- a/internal/service/cloudbackupschedule/resource_cloud_backup_schedule_migration_test.go +++ b/internal/service/cloudbackupschedule/resource_cloud_backup_schedule_migration_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - admin20240530 "go.mongodb.org/atlas-sdk/v20240530005/admin" + "go.mongodb.org/atlas-sdk/v20250312008/admin" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/plancheck" @@ -19,7 +19,7 @@ func TestMigBackupRSCloudBackupSchedule_basic(t *testing.T) { var ( clusterInfo = acc.GetClusterInfo(t, &acc.ClusterRequest{CloudBackup: true}) useYearly = mig.IsProviderVersionAtLeast("1.16.0") // attribute introduced in this version - config = configNewPolicies(&clusterInfo, &admin20240530.DiskBackupSnapshotSchedule{ + config = configNewPolicies(&clusterInfo, &admin.DiskBackupSnapshotSchedule20240805{ ReferenceHourOfDay: conversion.Pointer(0), ReferenceMinuteOfHour: conversion.Pointer(0), RestoreWindowDays: conversion.Pointer(7), @@ -65,12 +65,12 @@ func TestMigBackupRSCloudBackupSchedule_copySettings(t *testing.T) { terraformStr = clusterInfo.TerraformStr clusterResourceName = clusterInfo.ResourceName projectID = clusterInfo.ProjectID - copySettingsConfigWithRepSpecID = configCopySettings(terraformStr, projectID, clusterResourceName, false, true, &admin20240530.DiskBackupSnapshotSchedule{ + copySettingsConfigWithRepSpecID = configCopySettings(terraformStr, projectID, clusterResourceName, false, true, &admin.DiskBackupSnapshotSchedule20240805{ ReferenceHourOfDay: conversion.Pointer(3), ReferenceMinuteOfHour: conversion.Pointer(45), RestoreWindowDays: conversion.Pointer(1), }) - copySettingsConfigWithZoneID = configCopySettings(terraformStr, projectID, clusterResourceName, false, false, &admin20240530.DiskBackupSnapshotSchedule{ + copySettingsConfigWithZoneID = configCopySettings(terraformStr, projectID, clusterResourceName, false, false, &admin.DiskBackupSnapshotSchedule20240805{ ReferenceHourOfDay: conversion.Pointer(3), ReferenceMinuteOfHour: conversion.Pointer(45), RestoreWindowDays: conversion.Pointer(1), diff --git a/internal/service/cloudbackupschedule/resource_cloud_backup_schedule_test.go b/internal/service/cloudbackupschedule/resource_cloud_backup_schedule_test.go index 9bc22321c3..21628adf21 100644 --- a/internal/service/cloudbackupschedule/resource_cloud_backup_schedule_test.go +++ b/internal/service/cloudbackupschedule/resource_cloud_backup_schedule_test.go @@ -5,7 +5,7 @@ import ( "fmt" "testing" - admin20240530 "go.mongodb.org/atlas-sdk/v20240530005/admin" + "go.mongodb.org/atlas-sdk/v20250312008/admin" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" @@ -31,7 +31,7 @@ func TestAccBackupRSCloudBackupSchedule_basic(t *testing.T) { CheckDestroy: checkDestroy, Steps: []resource.TestStep{ { - Config: configNoPolicies(&clusterInfo, &admin20240530.DiskBackupSnapshotSchedule{ + Config: configNoPolicies(&clusterInfo, &admin.DiskBackupSnapshotSchedule20240805{ ReferenceHourOfDay: conversion.Pointer(3), ReferenceMinuteOfHour: conversion.Pointer(45), RestoreWindowDays: conversion.Pointer(4), @@ -59,7 +59,7 @@ func TestAccBackupRSCloudBackupSchedule_basic(t *testing.T) { ), }, { - Config: configNewPolicies(&clusterInfo, &admin20240530.DiskBackupSnapshotSchedule{ + Config: configNewPolicies(&clusterInfo, &admin.DiskBackupSnapshotSchedule20240805{ ReferenceHourOfDay: conversion.Pointer(0), ReferenceMinuteOfHour: conversion.Pointer(0), RestoreWindowDays: conversion.Pointer(7), @@ -102,7 +102,7 @@ func TestAccBackupRSCloudBackupSchedule_basic(t *testing.T) { ), }, { - Config: configAdvancedPolicies(&clusterInfo, &admin20240530.DiskBackupSnapshotSchedule{ + Config: configAdvancedPolicies(&clusterInfo, &admin.DiskBackupSnapshotSchedule20240805{ ReferenceHourOfDay: conversion.Pointer(0), ReferenceMinuteOfHour: conversion.Pointer(0), RestoreWindowDays: conversion.Pointer(7), @@ -203,7 +203,7 @@ func TestAccBackupRSCloudBackupSchedule_onePolicy(t *testing.T) { CheckDestroy: checkDestroy, Steps: []resource.TestStep{ { - Config: configDefault(&clusterInfo, &admin20240530.DiskBackupSnapshotSchedule{ + Config: configDefault(&clusterInfo, &admin.DiskBackupSnapshotSchedule20240805{ ReferenceHourOfDay: conversion.Pointer(3), ReferenceMinuteOfHour: conversion.Pointer(45), RestoreWindowDays: conversion.Pointer(4), @@ -237,7 +237,7 @@ func TestAccBackupRSCloudBackupSchedule_onePolicy(t *testing.T) { ), }, { - Config: configOnePolicy(&clusterInfo, &admin20240530.DiskBackupSnapshotSchedule{ + Config: configOnePolicy(&clusterInfo, &admin.DiskBackupSnapshotSchedule20240805{ ReferenceHourOfDay: conversion.Pointer(0), ReferenceMinuteOfHour: conversion.Pointer(0), RestoreWindowDays: conversion.Pointer(7), @@ -328,7 +328,7 @@ func TestAccBackupRSCloudBackupSchedule_copySettings_zoneId(t *testing.T) { CheckDestroy: checkDestroy, Steps: []resource.TestStep{ { - Config: configCopySettings(terraformStr, projectID, clusterResourceName, false, false, &admin20240530.DiskBackupSnapshotSchedule{ + Config: configCopySettings(terraformStr, projectID, clusterResourceName, false, false, &admin.DiskBackupSnapshotSchedule20240805{ ReferenceHourOfDay: conversion.Pointer(3), ReferenceMinuteOfHour: conversion.Pointer(45), RestoreWindowDays: conversion.Pointer(1), @@ -336,7 +336,7 @@ func TestAccBackupRSCloudBackupSchedule_copySettings_zoneId(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc(checksCreateAll...), }, { - Config: configCopySettings(terraformStr, projectID, clusterResourceName, true, false, &admin20240530.DiskBackupSnapshotSchedule{ + Config: configCopySettings(terraformStr, projectID, clusterResourceName, true, false, &admin.DiskBackupSnapshotSchedule20240805{ ReferenceHourOfDay: conversion.Pointer(3), ReferenceMinuteOfHour: conversion.Pointer(45), RestoreWindowDays: conversion.Pointer(1), @@ -358,7 +358,7 @@ func TestAccBackupRSCloudBackupScheduleImport_basic(t *testing.T) { CheckDestroy: checkDestroy, Steps: []resource.TestStep{ { - Config: configDefault(&clusterInfo, &admin20240530.DiskBackupSnapshotSchedule{ + Config: configDefault(&clusterInfo, &admin.DiskBackupSnapshotSchedule20240805{ ReferenceHourOfDay: conversion.Pointer(3), ReferenceMinuteOfHour: conversion.Pointer(45), RestoreWindowDays: conversion.Pointer(4), @@ -413,7 +413,7 @@ func TestAccBackupRSCloudBackupSchedule_azure(t *testing.T) { CheckDestroy: checkDestroy, Steps: []resource.TestStep{ { - Config: configAzure(&clusterInfo, &admin20240530.DiskBackupApiPolicyItem{ + Config: configAzure(&clusterInfo, &admin.BackupComplianceOnDemandPolicyItem{ FrequencyInterval: 1, RetentionUnit: "days", RetentionValue: 1, @@ -426,7 +426,7 @@ func TestAccBackupRSCloudBackupSchedule_azure(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "policy_item_hourly.0.retention_value", "1")), }, { - Config: configAzure(&clusterInfo, &admin20240530.DiskBackupApiPolicyItem{ + Config: configAzure(&clusterInfo, &admin.BackupComplianceOnDemandPolicyItem{ FrequencyInterval: 2, RetentionUnit: "days", RetentionValue: 3, @@ -492,7 +492,7 @@ func checkDestroy(s *terraform.State) error { return nil } -func configNoPolicies(info *acc.ClusterInfo, p *admin20240530.DiskBackupSnapshotSchedule) string { +func configNoPolicies(info *acc.ClusterInfo, p *admin.DiskBackupSnapshotSchedule20240805) string { return info.TerraformStr + fmt.Sprintf(` resource "mongodbatlas_cloud_backup_schedule" "schedule_test" { cluster_name = %[1]s @@ -510,7 +510,7 @@ func configNoPolicies(info *acc.ClusterInfo, p *admin20240530.DiskBackupSnapshot `, info.TerraformNameRef, info.ProjectID, p.GetReferenceHourOfDay(), p.GetReferenceMinuteOfHour(), p.GetRestoreWindowDays()) } -func configDefault(info *acc.ClusterInfo, p *admin20240530.DiskBackupSnapshotSchedule) string { +func configDefault(info *acc.ClusterInfo, p *admin.DiskBackupSnapshotSchedule20240805) string { return info.TerraformStr + fmt.Sprintf(` resource "mongodbatlas_cloud_backup_schedule" "schedule_test" { cluster_name = %[1]s @@ -554,7 +554,7 @@ func configDefault(info *acc.ClusterInfo, p *admin20240530.DiskBackupSnapshotSch `, info.TerraformNameRef, info.ProjectID, p.GetReferenceHourOfDay(), p.GetReferenceMinuteOfHour(), p.GetRestoreWindowDays()) } -func configCopySettings(terraformStr, projectID, clusterResourceName string, emptyCopySettings, useRepSpecID bool, p *admin20240530.DiskBackupSnapshotSchedule) string { +func configCopySettings(terraformStr, projectID, clusterResourceName string, emptyCopySettings, useRepSpecID bool, p *admin.DiskBackupSnapshotSchedule20240805) string { var copySettings string var dataSourceConfig string @@ -641,7 +641,7 @@ func configCopySettings(terraformStr, projectID, clusterResourceName string, emp `, terraformStr, projectID, clusterResourceName, p.GetReferenceHourOfDay(), p.GetReferenceMinuteOfHour(), p.GetRestoreWindowDays(), copySettings, dataSourceConfig) } -func configOnePolicy(info *acc.ClusterInfo, p *admin20240530.DiskBackupSnapshotSchedule) string { +func configOnePolicy(info *acc.ClusterInfo, p *admin.DiskBackupSnapshotSchedule20240805) string { return info.TerraformStr + fmt.Sprintf(` resource "mongodbatlas_cloud_backup_schedule" "schedule_test" { cluster_name = %[1]s @@ -660,7 +660,7 @@ func configOnePolicy(info *acc.ClusterInfo, p *admin20240530.DiskBackupSnapshotS `, info.TerraformNameRef, info.ProjectID, p.GetReferenceHourOfDay(), p.GetReferenceMinuteOfHour(), p.GetRestoreWindowDays()) } -func configNewPolicies(info *acc.ClusterInfo, p *admin20240530.DiskBackupSnapshotSchedule, useYearly bool) string { +func configNewPolicies(info *acc.ClusterInfo, p *admin.DiskBackupSnapshotSchedule20240805, useYearly bool) string { var strYearly string if useYearly { strYearly = ` @@ -711,7 +711,7 @@ func configNewPolicies(info *acc.ClusterInfo, p *admin20240530.DiskBackupSnapsho `, info.TerraformNameRef, info.ProjectID, p.GetReferenceHourOfDay(), p.GetReferenceMinuteOfHour(), p.GetRestoreWindowDays(), strYearly) } -func configAzure(info *acc.ClusterInfo, policy *admin20240530.DiskBackupApiPolicyItem) string { +func configAzure(info *acc.ClusterInfo, policy *admin.BackupComplianceOnDemandPolicyItem) string { return info.TerraformStr + fmt.Sprintf(` resource "mongodbatlas_cloud_backup_schedule" "schedule_test" { cluster_name = %[1]s @@ -731,7 +731,7 @@ func configAzure(info *acc.ClusterInfo, policy *admin20240530.DiskBackupApiPolic `, info.TerraformNameRef, info.ProjectID, policy.GetFrequencyInterval(), policy.GetRetentionUnit(), policy.GetRetentionValue()) } -func configAdvancedPolicies(info *acc.ClusterInfo, p *admin20240530.DiskBackupSnapshotSchedule) string { +func configAdvancedPolicies(info *acc.ClusterInfo, p *admin.DiskBackupSnapshotSchedule20240805) string { return info.TerraformStr + fmt.Sprintf(` resource "mongodbatlas_cloud_backup_schedule" "schedule_test" { cluster_name = %[1]s diff --git a/internal/service/eventtrigger/data_source_event_trigger.go b/internal/service/eventtrigger/data_source_event_trigger.go index 5cf8c318ff..bac40a4d0a 100644 --- a/internal/service/eventtrigger/data_source_event_trigger.go +++ b/internal/service/eventtrigger/data_source_event_trigger.go @@ -133,7 +133,7 @@ func DataSource() *schema.Resource { } func dataSourceMongoDBAtlasEventTriggerRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - conn, err := meta.(*config.MongoDBClient).GetRealmClient(ctx) + conn, err := meta.(*config.MongoDBClient).Realm.Get(ctx) if err != nil { return diag.FromErr(err) } diff --git a/internal/service/eventtrigger/data_source_event_triggers.go b/internal/service/eventtrigger/data_source_event_triggers.go index 120b42bdca..02295a331d 100644 --- a/internal/service/eventtrigger/data_source_event_triggers.go +++ b/internal/service/eventtrigger/data_source_event_triggers.go @@ -144,9 +144,8 @@ func PluralDataSource() *schema.Resource { } func dataSourceMongoDBAtlasEventTriggersRead(d *schema.ResourceData, meta any) error { - // Get client connection. ctx := context.Background() - conn, err := meta.(*config.MongoDBClient).GetRealmClient(ctx) + conn, err := meta.(*config.MongoDBClient).Realm.Get(ctx) if err != nil { return err } diff --git a/internal/service/eventtrigger/resource_event_trigger.go b/internal/service/eventtrigger/resource_event_trigger.go index b1e99aee41..1a90c8f3ab 100644 --- a/internal/service/eventtrigger/resource_event_trigger.go +++ b/internal/service/eventtrigger/resource_event_trigger.go @@ -210,7 +210,7 @@ func Resource() *schema.Resource { } func resourceMongoDBAtlasEventTriggersCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - conn, err := meta.(*config.MongoDBClient).GetRealmClient(ctx) + conn, err := meta.(*config.MongoDBClient).Realm.Get(ctx) if err != nil { return diag.FromErr(err) } @@ -312,7 +312,7 @@ func resourceMongoDBAtlasEventTriggersCreate(ctx context.Context, d *schema.Reso } func resourceMongoDBAtlasEventTriggersRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - conn, err := meta.(*config.MongoDBClient).GetRealmClient(ctx) + conn, err := meta.(*config.MongoDBClient).Realm.Get(ctx) if err != nil { return diag.FromErr(err) } @@ -402,7 +402,7 @@ func resourceMongoDBAtlasEventTriggersRead(ctx context.Context, d *schema.Resour } func resourceMongoDBAtlasEventTriggersUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - conn, err := meta.(*config.MongoDBClient).GetRealmClient(ctx) + conn, err := meta.(*config.MongoDBClient).Realm.Get(ctx) if err != nil { return diag.FromErr(err) } @@ -453,8 +453,7 @@ func resourceMongoDBAtlasEventTriggersUpdate(ctx context.Context, d *schema.Reso } func resourceMongoDBAtlasEventTriggersDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - // Get the client connection. - conn, err := meta.(*config.MongoDBClient).GetRealmClient(ctx) + conn, err := meta.(*config.MongoDBClient).Realm.Get(ctx) if err != nil { return diag.FromErr(err) } @@ -515,7 +514,7 @@ func flattenTriggerEventProcessorAWSEventBridge(eventProcessor map[string]any) [ } func resourceMongoDBAtlasEventTriggerImportState(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { - conn, err := meta.(*config.MongoDBClient).GetRealmClient(ctx) + conn, err := meta.(*config.MongoDBClient).Realm.Get(ctx) if err != nil { return nil, err } diff --git a/internal/service/eventtrigger/resource_event_trigger_test.go b/internal/service/eventtrigger/resource_event_trigger_test.go index 30f2c2734d..865690c6a7 100644 --- a/internal/service/eventtrigger/resource_event_trigger_test.go +++ b/internal/service/eventtrigger/resource_event_trigger_test.go @@ -484,7 +484,7 @@ func TestAccEventTrigger_functionBasic(t *testing.T) { func checkExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { ctx := context.Background() - conn, err := acc.MongoDBClient.GetRealmClient(ctx) + conn, err := acc.MongoDBClient.Realm.Get(ctx) if err != nil { return err } @@ -513,7 +513,7 @@ func checkExists(resourceName string) resource.TestCheckFunc { func checkDestroy(s *terraform.State) error { ctx := context.Background() - conn, err := acc.MongoDBClient.GetRealmClient(ctx) + conn, err := acc.MongoDBClient.Realm.Get(ctx) if err != nil { return err } diff --git a/internal/service/organization/resource_organization.go b/internal/service/organization/resource_organization.go index 3adda967a7..700446b469 100644 --- a/internal/service/organization/resource_organization.go +++ b/internal/service/organization/resource_organization.go @@ -113,7 +113,7 @@ func resourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag. if err := ValidateAPIKeyIsOrgOwner(conversion.ExpandStringList(d.Get("role_names").(*schema.Set).List())); err != nil { return diag.FromErr(err) } - conn := getAtlasV2Connection(ctx, d, meta) // Using provider credentials. + conn := getAtlasV2Connection(d, meta) // Using provider credentials. organization, resp, err := conn.OrganizationsApi.CreateOrg(ctx, newCreateOrganizationRequest(d)).Execute() if err != nil { if validate.StatusNotFound(resp) && !strings.Contains(err.Error(), "USER_NOT_FOUND") { @@ -128,7 +128,7 @@ func resourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag. if err := d.Set("public_key", organization.ApiKey.GetPublicKey()); err != nil { return diag.FromErr(fmt.Errorf("error setting `public_key`: %s", err)) } - conn = getAtlasV2Connection(ctx, d, meta) // Using new credentials from the created organization. + conn = getAtlasV2Connection(d, meta) // Using new credentials from the created organization. orgID := organization.Organization.GetId() _, _, errUpdate := conn.OrganizationsApi.UpdateOrgSettings(ctx, orgID, newOrganizationSettings(d)).Execute() if errUpdate != nil { @@ -146,7 +146,7 @@ func resourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag. } func resourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - conn := getAtlasV2Connection(ctx, d, meta) + conn := getAtlasV2Connection(d, meta) ids := conversion.DecodeStateID(d.Id()) orgID := ids["org_id"] @@ -194,7 +194,7 @@ func resourceRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Di } func resourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - conn := getAtlasV2Connection(ctx, d, meta) + conn := getAtlasV2Connection(d, meta) ids := conversion.DecodeStateID(d.Id()) orgID := ids["org_id"] for _, attr := range attrsCreateOnly { @@ -227,7 +227,7 @@ func resourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag. } func resourceDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - conn := getAtlasV2Connection(ctx, d, meta) + conn := getAtlasV2Connection(d, meta) ids := conversion.DecodeStateID(d.Id()) orgID := ids["org_id"] @@ -293,18 +293,21 @@ func ValidateAPIKeyIsOrgOwner(roles []string) error { // getAtlasV2Connection uses the created credentials for the organization if they exist. // Otherwise, it uses the provider credentials, e.g. if the resource was imported. -func getAtlasV2Connection(ctx context.Context, d *schema.ResourceData, meta any) *admin.APIClient { +func getAtlasV2Connection(d *schema.ResourceData, meta any) *admin.APIClient { + currentClient := meta.(*config.MongoDBClient) publicKey := d.Get("public_key").(string) privateKey := d.Get("private_key").(string) if publicKey == "" || privateKey == "" { - return meta.(*config.MongoDBClient).AtlasV2 + return currentClient.AtlasV2 } - cfg := config.Config{ - PublicKey: publicKey, - PrivateKey: privateKey, - BaseURL: meta.(*config.MongoDBClient).Config.BaseURL, - TerraformVersion: meta.(*config.MongoDBClient).Config.TerraformVersion, + c := &config.Credentials{ + PublicKey: publicKey, + PrivateKey: privateKey, + BaseURL: currentClient.BaseURL, } - clients, _ := cfg.NewClient(ctx) - return clients.AtlasV2 + newClient, err := config.NewClient(c, currentClient.TerraformVersion) + if err != nil { + return currentClient.AtlasV2 + } + return newClient.AtlasV2 } diff --git a/internal/service/organization/resource_organization_test.go b/internal/service/organization/resource_organization_test.go index bec6212f45..9d5eba3d60 100644 --- a/internal/service/organization/resource_organization_test.go +++ b/internal/service/organization/resource_organization_test.go @@ -427,18 +427,16 @@ func getTestClientWithNewOrgCreds(rs *terraform.ResourceState) (*admin.APIClient if rs.Primary.Attributes["public_key"] == "" { return nil, fmt.Errorf("no public_key is set") } - if rs.Primary.Attributes["private_key"] == "" { return nil, fmt.Errorf("no private_key is set") } - - cfg := config.Config{ + c := &config.Credentials{ PublicKey: rs.Primary.Attributes["public_key"], PrivateKey: rs.Primary.Attributes["private_key"], - BaseURL: acc.MongoDBClient.Config.BaseURL, + BaseURL: acc.MongoDBClient.BaseURL, } - clients, _ := cfg.NewClient(context.Background()) - return clients.AtlasV2, nil + client, _ := config.NewClient(c, acc.MongoDBClient.TerraformVersion) + return client.AtlasV2, nil } func TestValidateAPIKeyIsOrgOwner(t *testing.T) { diff --git a/internal/testutil/acc/factory.go b/internal/testutil/acc/factory.go index 1c46d1cd99..ed944cf387 100644 --- a/internal/testutil/acc/factory.go +++ b/internal/testutil/acc/factory.go @@ -1,7 +1,6 @@ package acc import ( - "context" "os" matlas "go.mongodb.org/atlas/mongodbatlas" @@ -42,12 +41,12 @@ func ConnV220241113() *admin20241113.APIClient { } func ConnV2UsingGov() *admin.APIClient { - cfg := config.Config{ + c := &config.Credentials{ PublicKey: os.Getenv("MONGODB_ATLAS_GOV_PUBLIC_KEY"), PrivateKey: os.Getenv("MONGODB_ATLAS_GOV_PRIVATE_KEY"), BaseURL: os.Getenv("MONGODB_ATLAS_GOV_BASE_URL"), } - client, _ := cfg.NewClient(context.Background()) + client, _ := config.NewClient(c, "") return client.AtlasV2 } @@ -57,11 +56,13 @@ func init() { return provider.MuxProviderFactory()(), nil }, } - cfg := config.Config{ + c := &config.Credentials{ PublicKey: os.Getenv("MONGODB_ATLAS_PUBLIC_KEY"), PrivateKey: os.Getenv("MONGODB_ATLAS_PRIVATE_KEY"), + ClientID: os.Getenv("MONGODB_ATLAS_CLIENT_ID"), + ClientSecret: os.Getenv("MONGODB_ATLAS_CLIENT_SECRET"), BaseURL: os.Getenv("MONGODB_ATLAS_BASE_URL"), RealmBaseURL: os.Getenv("MONGODB_REALM_BASE_URL"), } - MongoDBClient, _ = cfg.NewClient(context.Background()) + MongoDBClient, _ = config.NewClient(c, "") } diff --git a/internal/testutil/acc/independent_shard_scaling.go b/internal/testutil/acc/independent_shard_scaling.go index 7dc3f1e544..9de04da658 100644 --- a/internal/testutil/acc/independent_shard_scaling.go +++ b/internal/testutil/acc/independent_shard_scaling.go @@ -7,11 +7,12 @@ import ( "os" "github.com/mongodb-forks/digest" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" ) func GetIndependentShardScalingMode(ctx context.Context, projectID, clusterName string) (*string, *http.Response, error) { - baseURL := os.Getenv("MONGODB_ATLAS_BASE_URL") - req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"test/utils/auth/groups/"+projectID+"/clusters/"+clusterName+"/independentShardScalingMode", http.NoBody) + baseURL := config.NormalizeBaseURL(os.Getenv("MONGODB_ATLAS_BASE_URL")) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/test/utils/auth/groups/"+projectID+"/clusters/"+clusterName+"/independentShardScalingMode", http.NoBody) if err != nil { return nil, nil, err } diff --git a/internal/testutil/acc/pre_check.go b/internal/testutil/acc/pre_check.go index 8d98c621b5..0bc99d38b7 100644 --- a/internal/testutil/acc/pre_check.go +++ b/internal/testutil/acc/pre_check.go @@ -347,7 +347,7 @@ func PreCheckAwsMsk(tb testing.TB) { func PreCheckAccessToken(tb testing.TB) { tb.Helper() - if os.Getenv("MONGODB_ATLAS_OAUTH_TOKEN") == "" { - tb.Fatal("`MONGODB_ATLAS_OAUTH_TOKEN` must be set for Atlas Access Token acceptance testing") + if os.Getenv("MONGODB_ATLAS_ACCESS_TOKEN") == "" { + tb.Fatal("`MONGODB_ATLAS_ACCESS_TOKEN` must be set for Atlas Access Token acceptance testing") } } diff --git a/tools/generate-oauth2-token/main.go b/tools/generate-oauth2-token/main.go new file mode 100644 index 0000000000..6410b23a97 --- /dev/null +++ b/tools/generate-oauth2-token/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/mongodb/atlas-sdk-go/auth/clientcredentials" +) + +func main() { + baseURL := strings.TrimRight(os.Getenv("MONGODB_ATLAS_BASE_URL"), "/") + clientID := os.Getenv("MONGODB_ATLAS_CLIENT_ID") + clientSecret := os.Getenv("MONGODB_ATLAS_CLIENT_SECRET") + if baseURL == "" || clientID == "" || clientSecret == "" { + fmt.Fprintln(os.Stderr, "Error: MONGODB_ATLAS_BASE_URL, MONGODB_ATLAS_CLIENT_ID, and MONGODB_ATLAS_CLIENT_SECRET environment variables are required") + os.Exit(1) + } + conf := clientcredentials.NewConfig(clientID, clientSecret) + conf.TokenURL = baseURL + clientcredentials.TokenAPIPath + token, err := conf.Token(context.Background()) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to generate OAuth2 token: %v\n", err) + os.Exit(1) + } + fmt.Print(token.AccessToken) +}