diff --git a/.changelog/3636.txt b/.changelog/3636.txt new file mode 100644 index 0000000000..34aae725f0 --- /dev/null +++ b/.changelog/3636.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/mongodbatlas_encryption_at_rest: Supports role_id in google_cloud_kms_config +``` + +```release-note:enhancement +data-source/mongodbatlas_encryption_at_rest: Supports role_id in google_cloud_kms_config +``` diff --git a/.changelog/3637.txt b/.changelog/3637.txt new file mode 100644 index 0000000000..4a64f5b992 --- /dev/null +++ b/.changelog/3637.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/mongodbatlas_cloud_provider_access_setup: Adds support for GCP as a Cloud Provider. +``` + +```release-note:enhancement +data-source/mongodbatlas_cloud_provider_access_setup: Adds support for GCP as a Cloud Provider. +``` diff --git a/.changelog/3639.txt b/.changelog/3639.txt new file mode 100644 index 0000000000..09a03ae453 --- /dev/null +++ b/.changelog/3639.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/mongodbatlas_cloud_provider_access_authorization: Supports GCP cloud provider +``` diff --git a/.github/workflows/code-health.yml b/.github/workflows/code-health.yml index 0ae9c0870e..01040740c7 100644 --- a/.github/workflows/code-health.yml +++ b/.github/workflows/code-health.yml @@ -55,7 +55,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 with: - version: v2.3.1 # Also update GOLANGCI_VERSION variable in GNUmakefile when updating this version + version: v2.4.0 # Also update GOLANGCI_VERSION variable in GNUmakefile when updating this version - name: actionlint run: | make tools diff --git a/.github/workflows/pull-request-lint.yml b/.github/workflows/pull-request-lint.yml index cbafa4efe2..57b9e8cc23 100644 --- a/.github/workflows/pull-request-lint.yml +++ b/.github/workflows/pull-request-lint.yml @@ -17,7 +17,7 @@ jobs: permissions: pull-requests: write # Needed by sticky-pull-request-comment steps: - - uses: amannn/action-semantic-pull-request@7f33ba792281b034f64e96f4c0b5496782dd3b37 + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 id: lint_pr_title env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd53006a6..7d0e8f634f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## (Unreleased) +ENHANCEMENTS: + +* data-source/mongodbatlas_cloud_provider_access_setup: Adds support for GCP as a Cloud Provider. ([#3637](https://github.com/mongodb/terraform-provider-mongodbatlas/pull/3637)) +* data-source/mongodbatlas_encryption_at_rest: Supports role_id in google_cloud_kms_config ([#3636](https://github.com/mongodb/terraform-provider-mongodbatlas/pull/3636)) +* resource/mongodbatlas_cloud_provider_access_authorization: Supports GCP cloud provider ([#3639](https://github.com/mongodb/terraform-provider-mongodbatlas/pull/3639)) +* resource/mongodbatlas_cloud_provider_access_setup: Adds support for GCP as a Cloud Provider. ([#3637](https://github.com/mongodb/terraform-provider-mongodbatlas/pull/3637)) +* resource/mongodbatlas_encryption_at_rest: Supports role_id in google_cloud_kms_config ([#3636](https://github.com/mongodb/terraform-provider-mongodbatlas/pull/3636)) + ## 1.40.0 (August 21, 2025) ENHANCEMENTS: diff --git a/Makefile b/Makefile index 067b35db7c..a475f38f88 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ GITTAG=$(shell git describe --always --tags) VERSION=$(GITTAG:v%=%) LINKER_FLAGS=-s -w -X 'github.com/mongodb/terraform-provider-mongodbatlas/version.ProviderVersion=${VERSION}' -GOLANGCI_VERSION=v2.3.1 # Also update golangci-lint GH action in code-health.yml when updating this version +GOLANGCI_VERSION=v2.4.0 # Also update golangci-lint GH action in code-health.yml when updating this version export PATH := $(shell go env GOPATH)/bin:$(PATH) export SHELL := env PATH=$(PATH) /bin/bash diff --git a/docs/data-sources/cloud_provider_access_setup.md b/docs/data-sources/cloud_provider_access_setup.md index 5f3b9b53b2..7a621e007e 100644 --- a/docs/data-sources/cloud_provider_access_setup.md +++ b/docs/data-sources/cloud_provider_access_setup.md @@ -4,7 +4,7 @@ subcategory: "Cloud Provider Access" # Data Source: mongodbatlas_cloud_provider_access_setup -`mongodbatlas_cloud_provider_access_setup` allows you to get a single role for a provider access role setup, currently only AWS and Azure are supported. +`mongodbatlas_cloud_provider_access_setup` allows you to get a single role for a provider access role setup. Supported providers: AWS, AZURE and GCP. -> **NOTE:** Groups and projects are synonymous terms. You may find `groupId` in the official documentation. @@ -40,20 +40,33 @@ data "mongodbatlas_cloud_provider_access_setup" "single_setup" { role_id = mongodbatlas_cloud_provider_access_setup.test_role.role_id } ``` + +## Example Usage with GCP + +```terraform +resource "mongodbatlas_cloud_provider_access_setup" "test_role" { + project_id = "64259ee860c43338194b0f8e" + provider_name = "GCP" +} + +data "mongodbatlas_cloud_provider_access_setup" "single_setup" { + project_id = mongodbatlas_cloud_provider_access_setup.test_role.project_id + provider_name = mongodbatlas_cloud_provider_access_setup.test_role.provider_name + role_id = mongodbatlas_cloud_provider_access_setup.test_role.role_id +} +``` + ## Argument Reference * `project_id` - (Required) The unique ID for the project to get all Cloud Provider Access -* `provider_name` - (Required) cloud provider name, currently only AWS is supported -* `role_id` - (Required) unique role id among all the aws roles provided by mongodb atlas +* `provider_name` - (Required) cloud provider name. Supported values: `AWS`, `AZURE`, and `GCP`. +* `role_id` - (Required) unique role id among all the roles provided by MongoDB Atlas. ## Attributes Reference In addition to all arguments above, the following attributes are exported: * `id` - Autogenerated Unique ID for this data source. -* `aws` - aws related role information - * `atlas_assumed_role_external_id` - Unique external ID Atlas uses when assuming the IAM role in your AWS account. - * `atlas_aws_account_arn` - ARN associated with the Atlas AWS account used to assume IAM roles in your AWS account. * `aws_config` - aws related role information * `atlas_assumed_role_external_id` - Unique external ID Atlas uses when assuming the IAM role in your AWS account. * `atlas_aws_account_arn` - ARN associated with the Atlas AWS account used to assume IAM roles in your AWS account. @@ -61,6 +74,9 @@ In addition to all arguments above, the following attributes are exported: * `atlas_azure_app_id` - Azure Active Directory Application ID of Atlas. * `service_principal_id`- UUID string that identifies the Azure Service Principal. * `tenant_id` - UUID String that identifies the Azure Active Directory Tenant ID. + * `gcp_config` - gcp related configurations + * `status` - The status of the GCP cloud provider access setup. See [MongoDB Atlas API](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/operation/operation-getgroupcloudprovideraccess#operation-getgroupcloudprovideraccess-200-body-application-vnd-atlas-2023-01-01-json-gcp-object-status). + * `service_account_for_atlas` - The GCP service account email that Atlas uses. * `created_date` - Date on which this role was created. * `last_updated_date` - Date and time when this Azure Service Principal was last updated. This parameter expresses its value in the ISO 8601 timestamp format in UTC. diff --git a/docs/data-sources/encryption_at_rest.md b/docs/data-sources/encryption_at_rest.md index 984b6a8f13..4e3e76c09c 100644 --- a/docs/data-sources/encryption_at_rest.md +++ b/docs/data-sources/encryption_at_rest.md @@ -110,6 +110,8 @@ output "is_azure_encryption_at_rest_valid" { -> **NOTE:** It is possible to configure Atlas Encryption at Rest to communicate with Customer Managed Keys (Azure Key Vault or AWS KMS) over private network interfaces (Azure Private Link or AWS PrivateLink). This requires enabling the `azure_key_vault_config.require_private_networking` or the `aws_kms_config.require_private_networking` attribute, together with the configuration of the `mongodbatlas_encryption_at_rest_private_endpoint` resource. Please review the `mongodbatlas_encryption_at_rest_private_endpoint` resource for details. ### Configuring encryption at rest using customer key management in GCP +For authentication, you must provide either serviceAccountKey (static credentials) or roleId (service-account–based authentication). Once roleId is configured, serviceAccountKey is no longer supported. + ```terraform resource "mongodbatlas_encryption_at_rest" "test" { project_id = var.atlas_project_id @@ -185,6 +187,7 @@ Read-Only: - `enabled` (Boolean) Flag that indicates whether someone enabled encryption at rest for the specified project. To disable encryption at rest using customer key management and remove the configuration details, pass only this parameter with a value of `false`. - `key_version_resource_id` (String, Sensitive) Resource path that displays the key version resource ID for your Google Cloud KMS. +- `role_id` (String) Unique 24-hexadecimal digit string that identifies the Google Cloud Provider Access Role that MongoDB Cloud uses to access the Google Cloud KMS. - `service_account_key` (String, Sensitive) JavaScript Object Notation (JSON) object that contains the Google Cloud Key Management Service (KMS). Format the JSON as a string and not as an object. - `valid` (Boolean) Flag that indicates whether the Google Cloud Key Management Service (KMS) encryption key can encrypt and decrypt data. diff --git a/docs/resources/cloud_provider_access.md b/docs/resources/cloud_provider_access.md index 5d1a74fb4b..3c2665f39a 100644 --- a/docs/resources/cloud_provider_access.md +++ b/docs/resources/cloud_provider_access.md @@ -48,15 +48,25 @@ resource "mongodbatlas_cloud_provider_access_setup" "test_role" { ``` +## Example Usage with GCP + +```terraform + +resource "mongodbatlas_cloud_provider_access_setup" "test_role" { + project_id = "64259ee860c43338194b0f8e" + provider_name = "GCP" +} + +``` + ### Further Examples - [AWS Cloud Provider Access](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/master/examples/mongodbatlas_cloud_provider_access/aws) - [Azure Cloud Provider Access](https://github.com/mongodb/terraform-provider-mongodbatlas/tree/master/examples/mongodbatlas_cloud_provider_access/azure) - ## Argument Reference * `project_id` - (Required) The unique ID for the project -* `provider_name` - (Required) The cloud provider for which to create a new role. Currently only AWS and AZURE are supported. **WARNING** Changing the `provider_name` will result in destruction of the existing resource and the creation of a new resource. +* `provider_name` - (Required) The cloud provider for which to create a new role. Currently, AWS, AZURE and GCP are supported. **WARNING** Changing the `provider_name` will result in destruction of the existing resource and the creation of a new resource. * `azure_config` - azure related configurations * `atlas_azure_app_id` - Azure Active Directory Application ID of Atlas. This property is required when `provider_name = "AZURE".` * `service_principal_id`- UUID string that identifies the Azure Service Principal. This property is required when `provider_name = "AZURE".` @@ -68,6 +78,9 @@ resource "mongodbatlas_cloud_provider_access_setup" "test_role" { * `aws_config` - aws related arn roles * `atlas_assumed_role_external_id` - Unique external ID Atlas uses when assuming the IAM role in your AWS account. * `atlas_aws_account_arn` - ARN associated with the Atlas AWS account used to assume IAM roles in your AWS account. +* `gcp_config` - gcp related configuration + * `status` - The status of the GCP cloud provider access setup. See [MongoDB Atlas API](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/operation/operation-getgroupcloudprovideraccess#operation-getgroupcloudprovideraccess-200-body-application-vnd-atlas-2023-01-01-json-gcp-object-status). + * `service_account_for_atlas` - The GCP service account email that Atlas uses. * `created_date` - Date on which this role was created. * `last_updated_date` - Date and time when this Azure Service Principal was last updated. This parameter expresses its value in the ISO 8601 timestamp format in UTC. * `role_id` - Unique ID of this role. @@ -144,13 +157,18 @@ resource "mongodbatlas_cloud_provider_access_authorization" "auth_role" { Conditional * `aws` * `iam_assumed_role_arn` - (Required) ARN of the IAM Role that Atlas assumes when accessing resources in your AWS account. This value is required after the creation (register of the role) as part of [Set Up Unified AWS Access](https://docs.atlas.mongodb.com/security/set-up-unified-aws-access/#set-up-unified-aws-access). - +* `azure` + * `atlas_azure_app_id` - (Required) Azure Active Directory Application ID of Atlas. + * `service_principal_id` - (Required) UUID string that identifies the Azure Service Principal. + * `tenant_id` - (Required) UUID String that identifies the Azure Active Directory Tenant ID. ## Attributes Reference * `id` - Unique identifier used by terraform for internal management. * `authorized_date` - Date on which this role was authorized. * `feature_usages` - Atlas features this AWS IAM role is linked to. +* `gcp` + * `service_account_for_atlas` - Email address for the Google Service Account created by Atlas. diff --git a/docs/resources/encryption_at_rest.md b/docs/resources/encryption_at_rest.md index b21d29a2bb..cc93d20cca 100644 --- a/docs/resources/encryption_at_rest.md +++ b/docs/resources/encryption_at_rest.md @@ -138,6 +138,8 @@ Please review the [`mongodbatlas_encryption_at_rest_private_endpoint` resource d ### Configuring encryption at rest using customer key management in GCP +For authentication, you must provide either serviceAccountKey (static credentials) or roleId (service-account–based authentication). Once roleId is configured, serviceAccountKey is no longer supported. + ```terraform resource "mongodbatlas_encryption_at_rest" "test" { project_id = var.atlas_project_id @@ -219,6 +221,7 @@ Optional: - `enabled` (Boolean) Flag that indicates whether someone enabled encryption at rest for the specified project. To disable encryption at rest using customer key management and remove the configuration details, pass only this parameter with a value of `false`. - `key_version_resource_id` (String, Sensitive) Resource path that displays the key version resource ID for your Google Cloud KMS. +- `role_id` (String) Unique 24-hexadecimal digit string that identifies the Google Cloud Provider Access Role that MongoDB Cloud uses to access the Google Cloud KMS. - `service_account_key` (String, Sensitive) JavaScript Object Notation (JSON) object that contains the Google Cloud Key Management Service (KMS). Format the JSON as a string and not as an object. Read-Only: diff --git a/docs/resources/project.md b/docs/resources/project.md index 76d18a1a73..372d93c68b 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -58,7 +58,7 @@ resource "mongodbatlas_project" "test" { * `org_id` - (Required) The ID of the organization you want to create the project within. * `project_owner_id` - (Optional) Unique 24-hexadecimal digit string that identifies the Atlas user account to be granted the [Project Owner](https://docs.atlas.mongodb.com/reference/user-roles/#mongodb-authrole-Project-Owner) role on the specified project. If you set this parameter, it overrides the default value of the oldest [Organization Owner](https://docs.atlas.mongodb.com/reference/user-roles/#mongodb-authrole-Organization-Owner). * `tags` - (Optional) Map that contains key-value pairs between 1 to 255 characters in length for tagging and categorizing the project. See [below](#tags). -* `with_default_alerts_settings` - (Optional) It allows users to disable the creation of the default alert settings. By default, this flag is set to true. +* `with_default_alerts_settings` - (Optional) Flag that indicates whether the project is created with default alert settings. This setting cannot be updated after project creation. By default, this flag is set to true. * `is_collect_database_specifics_statistics_enabled` - (Optional) Flag that indicates whether to enable statistics in [cluster metrics](https://www.mongodb.com/docs/atlas/monitor-cluster-metrics/) collection for the project. By default, this flag is set to true. * `is_data_explorer_enabled` - (Optional) Flag that indicates whether to enable Data Explorer for the project. If enabled, you can query your database with an easy to use interface. When Data Explorer is disabled, you cannot terminate slow operations from the [Real-Time Performance Panel](https://www.mongodb.com/docs/atlas/real-time-performance-panel/#std-label-real-time-metrics-status-tab) or create indexes from the [Performance Advisor](https://www.mongodb.com/docs/atlas/performance-advisor/#std-label-performance-advisor). You can still view Performance Advisor recommendations, but you must create those indexes from [mongosh](https://www.mongodb.com/docs/mongodb-shell/#mongodb-binary-bin.mongosh). By default, this flag is set to true. * `is_extended_storage_sizes_enabled` - (Optional) Flag that indicates whether to enable extended storage sizes for the specified project. Clusters with extended storage sizes must be on AWS or GCP, and cannot span multiple regions. When extending storage size, initial syncs and cross-project snapshot restores will be slow. This setting should only be used as a measure of temporary relief; consider sharding if more storage is required. diff --git a/go.mod b/go.mod index 52dad8c189..c95deefb37 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/mongodb/terraform-provider-mongodbatlas -go 1.24.6 +go 1.25.0 require ( github.com/andygrunwald/go-jira/v2 v2.0.0-20240116150243-50d59fe116d6 @@ -25,7 +25,7 @@ require ( github.com/jarcoal/httpmock v1.4.1 github.com/mongodb-forks/digest v1.1.0 github.com/mongodb/atlas-sdk-go v1.0.1-0.20250825084037-c95a65f18752 - github.com/pb33f/libopenapi v0.25.3 + github.com/pb33f/libopenapi v0.25.8 github.com/sebdah/goldie/v2 v2.7.1 github.com/spf13/cast v1.9.2 github.com/stretchr/testify v1.11.0 diff --git a/go.sum b/go.sum index add92b6f40..49f76ee621 100644 --- a/go.sum +++ b/go.sum @@ -1215,8 +1215,8 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/openlyinc/pointy v1.1.2 h1:LywVV2BWC5Sp5v7FoP4bUD+2Yn5k0VNeRbU5vq9jUMY= github.com/openlyinc/pointy v1.1.2/go.mod h1:w2Sytx+0FVuMKn37xpXIAyBNhFNBIJGR/v2m7ik1WtM= -github.com/pb33f/libopenapi v0.25.3 h1:B0rf9Reo63tAx54gpoP9778Y84gk3JQoFtj7yg8vKfo= -github.com/pb33f/libopenapi v0.25.3/go.mod h1:IefJDi7uJpflLs2wEnkiier/Y21w+dEbOrMCP5LB2aw= +github.com/pb33f/libopenapi v0.25.8 h1:vA6NZAu6YClmpf4oceqdHowUkeOp79CODXGZAukhxQQ= +github.com/pb33f/libopenapi v0.25.8/go.mod h1:3MKMFLcYAnTgOuueDd2HIidMphtHHAhPdspgjKVVFq8= github.com/pb33f/ordered-map/v2 v2.2.0 h1:+6D6e0nkcEjVPh6kF48ynz2Cb+D/ECH/Q3AOunHtj7E= github.com/pb33f/ordered-map/v2 v2.2.0/go.mod h1:rAwLzJPAha8J3pY5otLGRbGH2L077wij3W/ftbgPwNs= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= diff --git a/internal/common/customplanmodifier/create_only.go b/internal/common/customplanmodifier/create_only.go index 4080f9d3da..5754a6cbc7 100644 --- a/internal/common/customplanmodifier/create_only.go +++ b/internal/common/customplanmodifier/create_only.go @@ -8,44 +8,88 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" planmodifier "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" ) -// CreateOnlyStringPlanModifier creates a plan modifier that prevents updates to string attributes. -func CreateOnlyStringPlanModifier() planmodifier.String { - return &createOnlyAttributePlanModifier{} +type CreateOnlyModifier interface { + planmodifier.String + planmodifier.Bool } -// CreateOnlyBoolPlanModifier creates a plan modifier that prevents updates to boolean attributes. -func CreateOnlyBoolPlanModifier() planmodifier.Bool { +// CreateOnlyAttributePlanModifier returns a plan modifier that ensures that update operations fails when the attribute is changed. +// This is useful for attributes only supported in create and not in update. +// It shows a helpful error message helping the user to update their config to match the state. +// Never use a schema.Default for create only attributes, instead use WithXXXDefault, the default will lead to plan changes that are not expected after import. +// Implement CopyFromPlan if the attribute is not in the API Response. +func CreateOnlyAttributePlanModifier() CreateOnlyModifier { return &createOnlyAttributePlanModifier{} } -// Plan modifier that implements create-only behavior for multiple attribute types -type createOnlyAttributePlanModifier struct{} +// CreateOnlyAttributePlanModifierWithBoolDefault sets a default value on create operation that will show in the plan. +// This avoids any custom logic in the resource "Create" handler. +// On update the default has no impact and the UseStateForUnknown behavior is observed instead. +// Always use Optional+Computed when using a default value. +func CreateOnlyAttributePlanModifierWithBoolDefault(b bool) CreateOnlyModifier { + return &createOnlyAttributePlanModifier{defaultBool: &b} +} + +type createOnlyAttributePlanModifier struct { + defaultBool *bool +} func (d *createOnlyAttributePlanModifier) Description(ctx context.Context) string { return d.MarkdownDescription(ctx) } func (d *createOnlyAttributePlanModifier) MarkdownDescription(ctx context.Context) string { - return "Ensures that update operations fail when attempting to modify a create-only attribute." + return "Ensures the update operation fails when updating an attribute. If the read after import don't equal the configuration value it will also raise an error." } -func (d *createOnlyAttributePlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { - validateCreateOnly(req.PlanValue, req.StateValue, req.Path, &resp.Diagnostics) +func isCreate(t *tfsdk.State) bool { + return t.Raw.IsNull() +} + +func (d *createOnlyAttributePlanModifier) UseDefault() bool { + return d.defaultBool != nil } func (d *createOnlyAttributePlanModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { - validateCreateOnly(req.PlanValue, req.StateValue, req.Path, &resp.Diagnostics) + if isCreate(&req.State) { + if !IsKnown(req.PlanValue) && d.UseDefault() { + resp.PlanValue = types.BoolPointerValue(d.defaultBool) + } + return + } + if isUpdated(req.StateValue, req.PlanValue) { + d.addDiags(&resp.Diagnostics, req.Path, req.StateValue) + } + if !IsKnown(req.PlanValue) { + resp.PlanValue = req.StateValue + } } -// validateCreateOnly checks if an attribute value has changed and adds an error if it has -func validateCreateOnly(planValue, stateValue attr.Value, attrPath path.Path, diagnostics *diag.Diagnostics, -) { - if !stateValue.IsNull() && !stateValue.Equal(planValue) { - diagnostics.AddError( - fmt.Sprintf("%s cannot be updated", attrPath), - fmt.Sprintf("%s cannot be updated", attrPath), - ) +func (d *createOnlyAttributePlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + if isCreate(&req.State) { + return + } + if isUpdated(req.StateValue, req.PlanValue) { + d.addDiags(&resp.Diagnostics, req.Path, req.StateValue) } + if !IsKnown(req.PlanValue) { + resp.PlanValue = req.StateValue + } +} + +func isUpdated(state, plan attr.Value) bool { + if !IsKnown(plan) { + return false + } + return !state.Equal(plan) +} + +func (d *createOnlyAttributePlanModifier) addDiags(diags *diag.Diagnostics, attrPath path.Path, stateValue attr.Value) { + message := fmt.Sprintf("%s cannot be updated or set after import, remove it from the configuration or use the state value (see below).", attrPath) + detail := fmt.Sprintf("The current state value is %s", stateValue) + diags.AddError(message, detail) } diff --git a/internal/common/customplanmodifier/is_known.go b/internal/common/customplanmodifier/is_known.go new file mode 100644 index 0000000000..8eedd62889 --- /dev/null +++ b/internal/common/customplanmodifier/is_known.go @@ -0,0 +1,8 @@ +package customplanmodifier + +import "github.com/hashicorp/terraform-plugin-framework/attr" + +// IsKnown returns true if the attribute is known (not null or unknown). Note that !IsKnown is not the same as IsUnknown because null is !IsKnown but not IsUnknown. +func IsKnown(attribute attr.Value) bool { + return !attribute.IsNull() && !attribute.IsUnknown() +} diff --git a/internal/service/cloudprovideraccess/data_source_cloud_provider_access_setup.go b/internal/service/cloudprovideraccess/data_source_cloud_provider_access_setup.go index 33f7892fe3..61e3418688 100644 --- a/internal/service/cloudprovideraccess/data_source_cloud_provider_access_setup.go +++ b/internal/service/cloudprovideraccess/data_source_cloud_provider_access_setup.go @@ -7,7 +7,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/mongodb/terraform-provider-mongodbatlas/internal/config" ) @@ -20,9 +19,8 @@ func DataSourceSetup() *schema.Resource { Required: true, }, "provider_name": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice([]string{"AWS", "AZURE"}, false), + Type: schema.TypeString, + Required: true, }, "role_id": { Type: schema.TypeString, @@ -71,6 +69,22 @@ func DataSourceSetup() *schema.Resource { }, }, }, + "gcp_config": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "status": { + Type: schema.TypeString, + Computed: true, + }, + "service_account_for_atlas": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, "created_date": { Type: schema.TypeString, Computed: true, @@ -93,7 +107,11 @@ func dataSourceMongoDBAtlasCloudProviderAccessSetupRead(ctx context.Context, d * return diag.FromErr(fmt.Errorf(ErrorCloudProviderGetRead, err)) } - roleSchema := roleToSchemaSetup(role) + roleSchema, err := roleToSchemaSetup(role) + if err != nil { + return diag.FromErr(fmt.Errorf(ErrorCloudProviderGetRead, err)) + } + for key, val := range roleSchema { if err := d.Set(key, val); err != nil { return diag.FromErr(fmt.Errorf(ErrorCloudProviderGetRead, err)) diff --git a/internal/service/cloudprovideraccess/resource_cloud_provider_access_authorization.go b/internal/service/cloudprovideraccess/resource_cloud_provider_access_authorization.go index 5b468c5531..baad3da9da 100644 --- a/internal/service/cloudprovideraccess/resource_cloud_provider_access_authorization.go +++ b/internal/service/cloudprovideraccess/resource_cloud_provider_access_authorization.go @@ -70,6 +70,18 @@ func ResourceAuthorization() *schema.Resource { }, }, }, + "gcp": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "service_account_for_atlas": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, "feature_usages": { Type: schema.TypeList, Elem: featureUsagesSchema(), @@ -168,6 +180,10 @@ func resourceCloudProviderAccessAuthorizationUpdate(ctx context.Context, d *sche } if d.HasChange("aws") || d.HasChange("azure") { + // Re-authorize the role with updated AWS or Azure configuration. + // GCP authorization only requires a role ID and has no additional configuration to update. + // Therefore, "updating" a GCP role would effectively be creating a new authorization, + // which should be handled by creating a new resource rather than updating an existing one. return authorizeRole(ctx, conn, d, projectID, targetRole) } @@ -186,6 +202,7 @@ func roleToSchemaAuthorization(role *admin.CloudProviderAccessRole) map[string]a "iam_assumed_role_arn": role.GetIamAssumedRoleArn(), }}, "authorized_date": conversion.TimeToString(role.GetAuthorizedDate()), + "gcp": []any{map[string]any{}}, } if role.ProviderName == "AZURE" { @@ -197,6 +214,15 @@ func roleToSchemaAuthorization(role *admin.CloudProviderAccessRole) map[string]a "tenant_id": role.GetTenantId(), }}, "authorized_date": conversion.TimeToString(role.GetAuthorizedDate()), + "gcp": []any{map[string]any{}}, + } + } + if role.ProviderName == "GCP" { + out = map[string]any{ + "role_id": role.GetRoleId(), + "gcp": []any{map[string]any{ + "service_account_for_atlas": role.GetGcpServiceAccountForAtlas(), + }}, } } @@ -281,6 +307,7 @@ func authorizeRole(ctx context.Context, client *admin.APIClient, d *schema.Resou req.SetServicePrincipalId(targetRole.GetServicePrincipalId()) roleID = targetRole.GetId() } + // No specific GCP config is needed, only providerName and roleID are needed var role *admin.CloudProviderAccessRole var err error diff --git a/internal/service/cloudprovideraccess/resource_cloud_provider_access_setup.go b/internal/service/cloudprovideraccess/resource_cloud_provider_access_setup.go index 29897845a5..4640118e24 100644 --- a/internal/service/cloudprovideraccess/resource_cloud_provider_access_setup.go +++ b/internal/service/cloudprovideraccess/resource_cloud_provider_access_setup.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/constant" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/validate" @@ -46,10 +45,9 @@ func ResourceSetup() *schema.Resource { Required: true, }, "provider_name": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice([]string{constant.AWS, constant.AZURE}, false), - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, }, "aws_config": { Type: schema.TypeList, @@ -87,6 +85,22 @@ func ResourceSetup() *schema.Resource { }, }, }, + "gcp_config": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "status": { + Type: schema.TypeString, + Computed: true, + }, + "service_account_for_atlas": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, "created_date": { Type: schema.TypeString, Computed: true, @@ -119,7 +133,10 @@ func resourceCloudProviderAccessSetupRead(ctx context.Context, d *schema.Resourc return diag.FromErr(fmt.Errorf(ErrorCloudProviderGetRead, err)) } - roleSchema := roleToSchemaSetup(role) + roleSchema, err := roleToSchemaSetup(role) + if err != nil { + return diag.Errorf(errorCloudProviderAccessCreate, err) + } for key, val := range roleSchema { if err := d.Set(key, val); err != nil { return diag.FromErr(fmt.Errorf(ErrorCloudProviderGetRead, err)) @@ -156,7 +173,10 @@ func resourceCloudProviderAccessSetupCreate(ctx context.Context, d *schema.Resou } // once multiple providers enable here do a switch, select for provider type - roleSchema := roleToSchemaSetup(role) + roleSchema, err := roleToSchemaSetup(role) + if err != nil { + return diag.Errorf(errorCloudProviderAccessCreate, err) + } resourceID := role.GetRoleId() if role.ProviderName == constant.AZURE { @@ -197,39 +217,51 @@ func resourceCloudProviderAccessSetupDelete(ctx context.Context, d *schema.Resou return diag.FromErr(fmt.Errorf(errorCloudProviderAccessDelete, err)) } - d.SetId("") d.SetId("") return nil } -func roleToSchemaSetup(role *admin.CloudProviderAccessRole) map[string]any { - if role.ProviderName == "AWS" { - out := map[string]any{ +func roleToSchemaSetup(role *admin.CloudProviderAccessRole) (map[string]any, error) { + switch role.ProviderName { + case constant.AWS: + return map[string]any{ "provider_name": role.GetProviderName(), "aws_config": []any{map[string]any{ "atlas_aws_account_arn": role.GetAtlasAWSAccountArn(), "atlas_assumed_role_external_id": role.GetAtlasAssumedRoleExternalId(), }}, + "gcp_config": []any{map[string]any{}}, "created_date": conversion.TimeToString(role.GetCreatedDate()), "role_id": role.GetRoleId(), - } - return out - } - - out := map[string]any{ - "provider_name": role.ProviderName, - "azure_config": []any{map[string]any{ - "atlas_azure_app_id": role.GetAtlasAzureAppId(), - "service_principal_id": role.GetServicePrincipalId(), - "tenant_id": role.GetTenantId(), - }}, - "aws_config": []any{map[string]any{}}, - "created_date": conversion.TimeToString(role.GetCreatedDate()), - "last_updated_date": conversion.TimeToString(role.GetLastUpdatedDate()), - "role_id": role.GetId(), + }, nil + case constant.AZURE: + return map[string]any{ + "provider_name": role.ProviderName, + "azure_config": []any{map[string]any{ + "atlas_azure_app_id": role.GetAtlasAzureAppId(), + "service_principal_id": role.GetServicePrincipalId(), + "tenant_id": role.GetTenantId(), + }}, + "aws_config": []any{map[string]any{}}, + "gcp_config": []any{map[string]any{}}, + "created_date": conversion.TimeToString(role.GetCreatedDate()), + "last_updated_date": conversion.TimeToString(role.GetLastUpdatedDate()), + "role_id": role.GetId(), + }, nil + case constant.GCP: + return map[string]any{ + "provider_name": role.GetProviderName(), + "gcp_config": []any{map[string]any{ + "status": role.GetStatus(), + "service_account_for_atlas": role.GetGcpServiceAccountForAtlas(), + }}, + "aws_config": []any{map[string]any{}}, + "role_id": role.GetId(), + "created_date": conversion.TimeToString(role.GetCreatedDate()), + }, nil + default: + return nil, fmt.Errorf("unsupported provider: %s", role.GetProviderName()) } - - return out } func resourceCloudProviderAccessSetupImportState(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { diff --git a/internal/service/cloudprovideraccess/resource_cloud_provider_access_setup_test.go b/internal/service/cloudprovideraccess/resource_cloud_provider_access_setup_test.go index 824cdcad55..8c34f39dfd 100644 --- a/internal/service/cloudprovideraccess/resource_cloud_provider_access_setup_test.go +++ b/internal/service/cloudprovideraccess/resource_cloud_provider_access_setup_test.go @@ -62,6 +62,33 @@ func TestAccCloudProviderAccessSetupAzure_basic(t *testing.T) { }, ) } +func TestAccCloudProviderAccessSetupGCP_basic(t *testing.T) { + acc.SkipTestForCI(t) // Code needs to support long running operations for successful test: CLOUDP-341440 + var ( + resourceName = "mongodbatlas_cloud_provider_access_setup.test" + dataSourceName = "data.mongodbatlas_cloud_provider_access_setup.test" + projectID = acc.ProjectIDExecution(t) + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.PreCheckGCPEnv(t) }, + ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, + Steps: []resource.TestStep{ + { + Config: configSetupGCP(projectID), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "role_id"), + resource.TestCheckResourceAttrSet(resourceName, "gcp_config.0.service_account_for_atlas"), + resource.TestCheckResourceAttr(resourceName, "gcp_config.0.status", "COMPLETE"), + + resource.TestCheckResourceAttrSet(dataSourceName, "role_id"), + resource.TestCheckResourceAttrSet(dataSourceName, "gcp_config.0.service_account_for_atlas"), + resource.TestCheckResourceAttr(dataSourceName, "gcp_config.0.status", "COMPLETE"), + ), + }, + }, + }) +} func basicSetupTestCase(tb testing.TB) *resource.TestCase { tb.Helper() @@ -113,6 +140,21 @@ func configSetupAWS(projectID string) string { `, projectID) } +func configSetupGCP(projectID string) string { + return fmt.Sprintf(` + resource "mongodbatlas_cloud_provider_access_setup" "test" { + project_id = %[1]q + provider_name = "GCP" + } + + data "mongodbatlas_cloud_provider_access_setup" "test" { + project_id = mongodbatlas_cloud_provider_access_setup.test.project_id + provider_name = mongodbatlas_cloud_provider_access_setup.test.provider_name + role_id = mongodbatlas_cloud_provider_access_setup.test.role_id + } + `, projectID) +} + func checkExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] diff --git a/internal/service/encryptionatrest/data_source_schema.go b/internal/service/encryptionatrest/data_source_schema.go index 258ff8c5bf..ead8063c3e 100644 --- a/internal/service/encryptionatrest/data_source_schema.go +++ b/internal/service/encryptionatrest/data_source_schema.go @@ -128,6 +128,10 @@ func DataSourceSchema(ctx context.Context) schema.Schema { Computed: true, MarkdownDescription: "Flag that indicates whether the Google Cloud Key Management Service (KMS) encryption key can encrypt and decrypt data.", }, + "role_id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Unique 24-hexadecimal digit string that identifies the Google Cloud Provider Access Role that MongoDB Cloud uses to access the Google Cloud KMS.", + }, }, Computed: true, MarkdownDescription: "Details that define the configuration of Encryption at Rest using Google Cloud Key Management Service (KMS).", diff --git a/internal/service/encryptionatrest/model.go b/internal/service/encryptionatrest/model.go index 50ffa5a388..ecd3357ab6 100644 --- a/internal/service/encryptionatrest/model.go +++ b/internal/service/encryptionatrest/model.go @@ -102,6 +102,7 @@ func NewTFGcpKmsConfigItem(gcpKms *admin.GoogleCloudKMS) *TFGcpKmsConfigModel { KeyVersionResourceID: types.StringValue(gcpKms.GetKeyVersionResourceID()), ServiceAccountKey: conversion.StringNullIfEmpty(gcpKms.GetServiceAccountKey()), Valid: types.BoolPointerValue(gcpKms.Valid), + RoleID: conversion.StringNullIfEmpty(gcpKms.GetRoleId()), } } @@ -134,6 +135,7 @@ func NewAtlasGcpKms(tfGcpKmsConfigSlice []TFGcpKmsConfigModel) *admin.GoogleClou Enabled: v.Enabled.ValueBoolPointer(), ServiceAccountKey: v.ServiceAccountKey.ValueStringPointer(), KeyVersionResourceID: v.KeyVersionResourceID.ValueStringPointer(), + RoleId: v.RoleID.ValueStringPointer(), } } diff --git a/internal/service/encryptionatrest/model_test.go b/internal/service/encryptionatrest/model_test.go index 2c07646ebd..84d08f1d60 100644 --- a/internal/service/encryptionatrest/model_test.go +++ b/internal/service/encryptionatrest/model_test.go @@ -76,11 +76,13 @@ var ( Enabled: &enabled, KeyVersionResourceID: &keyVersionResourceID, ServiceAccountKey: &serviceAccountKey, + RoleId: &roleID, } TfGcpKmsConfigModel = encryptionatrest.TFGcpKmsConfigModel{ Enabled: types.BoolValue(enabled), KeyVersionResourceID: types.StringValue(keyVersionResourceID), ServiceAccountKey: types.StringValue(serviceAccountKey), + RoleID: types.StringValue(roleID), } EncryptionAtRest = &admin.EncryptionAtRest{ AwsKms: AWSKMSConfiguration, diff --git a/internal/service/encryptionatrest/resource.go b/internal/service/encryptionatrest/resource.go index 6923b6be12..63674658b1 100644 --- a/internal/service/encryptionatrest/resource.go +++ b/internal/service/encryptionatrest/resource.go @@ -87,6 +87,7 @@ type TFAzureKeyVaultConfigModel struct { type TFGcpKmsConfigModel struct { ServiceAccountKey types.String `tfsdk:"service_account_key"` KeyVersionResourceID types.String `tfsdk:"key_version_resource_id"` + RoleID types.String `tfsdk:"role_id"` Enabled types.Bool `tfsdk:"enabled"` Valid types.Bool `tfsdk:"valid"` } @@ -259,6 +260,10 @@ func (r *encryptionAtRestRS) Schema(ctx context.Context, req resource.SchemaRequ Computed: true, MarkdownDescription: "Flag that indicates whether the Google Cloud Key Management Service (KMS) encryption key can encrypt and decrypt data.", }, + "role_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Unique 24-hexadecimal digit string that identifies the Google Cloud Provider Access Role that MongoDB Cloud uses to access the Google Cloud KMS.", + }, }, }, }, diff --git a/internal/service/encryptionatrest/resource_migration_test.go b/internal/service/encryptionatrest/resource_migration_test.go index bc69b5fdcd..9b7ffabaac 100644 --- a/internal/service/encryptionatrest/resource_migration_test.go +++ b/internal/service/encryptionatrest/resource_migration_test.go @@ -119,7 +119,7 @@ func TestMigEncryptionAtRest_basicGCP(t *testing.T) { ) resource.Test(t, resource.TestCase{ - PreCheck: func() { mig.PreCheck(t); acc.PreCheckGPCEnv(t) }, + PreCheck: func() { mig.PreCheck(t); acc.PreCheckGCPEnv(t) }, CheckDestroy: acc.EARDestroy, Steps: []resource.TestStep{ { diff --git a/internal/service/encryptionatrest/resource_test.go b/internal/service/encryptionatrest/resource_test.go index be2fa573b1..ddbe083457 100644 --- a/internal/service/encryptionatrest/resource_test.go +++ b/internal/service/encryptionatrest/resource_test.go @@ -169,7 +169,7 @@ func TestAccEncryptionAtRest_basicGCP(t *testing.T) { ) resource.Test(t, resource.TestCase{ - PreCheck: func() { acc.PreCheck(t); acc.PreCheckGPCEnv(t) }, + PreCheck: func() { acc.PreCheck(t); acc.PreCheckGCPEnv(t) }, ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, CheckDestroy: acc.EARDestroy, Steps: []resource.TestStep{ @@ -215,6 +215,32 @@ func TestAccEncryptionAtRest_basicGCP(t *testing.T) { }) } +func TestAccEncryptionAtRest_basicGCPWithRole(t *testing.T) { + acc.SkipTestForCI(t) // needs GCP configuration + + var ( + projectID = os.Getenv("MONGODB_ATLAS_PROJECT_ID") + + googleCloudKms = admin.GoogleCloudKMS{ + Enabled: conversion.Pointer(true), + RoleId: conversion.StringPtr(os.Getenv("GCP_ROLE_ID")), + KeyVersionResourceID: conversion.StringPtr(os.Getenv("GCP_KEY_VERSION_RESOURCE_ID")), + } + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.PreCheck(t); acc.PreCheckGCPEnvWithRole(t) }, + ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, + CheckDestroy: acc.EARDestroy, + Steps: []resource.TestStep{ + { + Config: configGoogleCloudKmsWithRole(projectID, &googleCloudKms, true), + Check: checkEARResourceGCPWithRole(projectID), + }, + }, + }) +} + func TestAccEncryptionAtRestWithRole_basicAWS(t *testing.T) { acc.SkipTestForCI(t) // needs AWS configuration. This test case is similar to TestAccEncryptionAtRest_basicAWS except that it creates it's own AWS resources such as IAM roles, cloud provider access, etc so we don't need to run this in CI but may be used for local testing. @@ -516,6 +542,25 @@ func configGoogleCloudKms(projectID string, google *admin.GoogleCloudKMS, useDat return config } +func configGoogleCloudKmsWithRole(projectID string, google *admin.GoogleCloudKMS, useDatasource bool) string { + config := fmt.Sprintf(` + resource "mongodbatlas_encryption_at_rest" "test" { + project_id = "%s" + + google_cloud_kms_config { + enabled = %t + role_id = "%s" + key_version_resource_id = "%s" + } + } + `, projectID, *google.Enabled, google.GetRoleId(), google.GetKeyVersionResourceID()) + + if useDatasource { + return fmt.Sprintf(`%s %s`, config, acc.EARDatasourceConfig()) + } + return config +} + func testAccMongoDBAtlasEncryptionAtRestConfigAwsKmsWithRole(projectID, awsIAMRoleName, awsIAMRolePolicyName, awsKeyName string, awsEar *admin.AWSKMSConfiguration) string { test := fmt.Sprintf(` locals { @@ -620,3 +665,19 @@ func checkEARResourceAWS(projectID string, enabledForSearchNodes bool, awsKmsAtt acc.EARCheckResourceAttr(datasourceName, "aws_kms_config.", awsKmsAttrMap), ) } + +func checkEARResourceGCPWithRole(projectID string) resource.TestCheckFunc { + return resource.ComposeAggregateTestCheckFunc( + acc.CheckEARExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "project_id", projectID), + resource.TestCheckResourceAttr(resourceName, "google_cloud_kms_config.0.role_id", os.Getenv("GCP_ROLE_ID")), + resource.TestCheckResourceAttr(resourceName, "google_cloud_kms_config.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "google_cloud_kms_config.0.valid", "true"), + resource.TestCheckResourceAttrSet(resourceName, "google_cloud_kms_config.0.key_version_resource_id"), + + resource.TestCheckResourceAttr(datasourceName, "project_id", projectID), + resource.TestCheckResourceAttr(datasourceName, "google_cloud_kms_config.enabled", "true"), + resource.TestCheckResourceAttr(datasourceName, "google_cloud_kms_config.valid", "true"), + resource.TestCheckResourceAttrSet(datasourceName, "google_cloud_kms_config.key_version_resource_id"), + ) +} diff --git a/internal/service/encryptionatrestprivateendpoint/resource_schema.go b/internal/service/encryptionatrestprivateendpoint/resource_schema.go index e04b0ee1fa..42184d3eae 100644 --- a/internal/service/encryptionatrestprivateendpoint/resource_schema.go +++ b/internal/service/encryptionatrestprivateendpoint/resource_schema.go @@ -48,7 +48,7 @@ func ResourceSchema(ctx context.Context) schema.Schema { "delete_on_create_timeout": schema.BoolAttribute{ Optional: true, PlanModifiers: []planmodifier.Bool{ - customplanmodifier.CreateOnlyBoolPlanModifier(), + customplanmodifier.CreateOnlyAttributePlanModifier(), }, MarkdownDescription: "Indicates whether to delete the resource being created if a timeout is reached when waiting for completion. When set to `true` and timeout occurs, it triggers the deletion and returns immediately without waiting for deletion to complete. When set to `false`, the timeout will not trigger resource deletion. If you suspect a transient error when the value is `true`, wait before retrying to allow resource deletion to finish. Default is `true`.", }, diff --git a/internal/service/flexcluster/resource.go b/internal/service/flexcluster/resource.go index 17ee5dea7e..0fc0bc4fc9 100644 --- a/internal/service/flexcluster/resource.go +++ b/internal/service/flexcluster/resource.go @@ -80,8 +80,7 @@ func (r *rs) Create(ctx context.Context, req resource.CreateRequest, resp *resou flexClusterResp, err := CreateFlexCluster(ctx, projectID, clusterName, flexClusterReq, connV2.FlexClustersApi, &createTimeout) // Handle timeout with cleanup logic - deleteOnCreateTimeout := cleanup.ResolveDeleteOnCreateTimeout(tfModel.DeleteOnCreateTimeout) - err = cleanup.HandleCreateTimeout(deleteOnCreateTimeout, err, func(ctxCleanup context.Context) error { + err = cleanup.HandleCreateTimeout(tfModel.DeleteOnCreateTimeout.ValueBool(), err, func(ctxCleanup context.Context) error { cleanResp, cleanErr := r.Client.AtlasV2.FlexClustersApi.DeleteFlexCluster(ctxCleanup, projectID, clusterName).Execute() if validate.StatusNotFound(cleanResp) { return nil diff --git a/internal/service/flexcluster/resource_schema.go b/internal/service/flexcluster/resource_schema.go index a382145ca2..e471f8c79b 100644 --- a/internal/service/flexcluster/resource_schema.go +++ b/internal/service/flexcluster/resource_schema.go @@ -22,14 +22,14 @@ func ResourceSchema(ctx context.Context) schema.Schema { "project_id": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{ - customplanmodifier.CreateOnlyStringPlanModifier(), + customplanmodifier.CreateOnlyAttributePlanModifier(), }, MarkdownDescription: "Unique 24-hexadecimal character string that identifies the project.", }, "name": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{ - customplanmodifier.CreateOnlyStringPlanModifier(), + customplanmodifier.CreateOnlyAttributePlanModifier(), }, MarkdownDescription: "Human-readable label that identifies the instance.", }, @@ -38,7 +38,7 @@ func ResourceSchema(ctx context.Context) schema.Schema { "backing_provider_name": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{ - customplanmodifier.CreateOnlyStringPlanModifier(), + customplanmodifier.CreateOnlyAttributePlanModifier(), }, MarkdownDescription: "Cloud service provider on which MongoDB Cloud provisioned the flex cluster.", }, @@ -59,7 +59,7 @@ func ResourceSchema(ctx context.Context) schema.Schema { "region_name": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{ - customplanmodifier.CreateOnlyStringPlanModifier(), + customplanmodifier.CreateOnlyAttributePlanModifier(), }, MarkdownDescription: "Human-readable label that identifies the geographic location of your MongoDB flex cluster. The region you choose can affect network latency for clients accessing your databases. For a complete list of region names, see [AWS](https://docs.atlas.mongodb.com/reference/amazon-aws/#std-label-amazon-aws), [GCP](https://docs.atlas.mongodb.com/reference/google-gcp/), and [Azure](https://docs.atlas.mongodb.com/reference/microsoft-azure/).", }, @@ -148,8 +148,9 @@ func ResourceSchema(ctx context.Context) schema.Schema { }, "delete_on_create_timeout": schema.BoolAttribute{ Optional: true, + Computed: true, PlanModifiers: []planmodifier.Bool{ - customplanmodifier.CreateOnlyBoolPlanModifier(), + customplanmodifier.CreateOnlyAttributePlanModifierWithBoolDefault(true), }, MarkdownDescription: "Indicates whether to delete the resource being created if a timeout is reached when waiting for completion. When set to `true` and timeout occurs, it triggers the deletion and returns immediately without waiting for deletion to complete. When set to `false`, the timeout will not trigger resource deletion. If you suspect a transient error when the value is `true`, wait before retrying to allow resource deletion to finish. Default is `true`.", }, diff --git a/internal/service/flexcluster/resource_test.go b/internal/service/flexcluster/resource_test.go index 2cef4ca1cc..b787ada201 100644 --- a/internal/service/flexcluster/resource_test.go +++ b/internal/service/flexcluster/resource_test.go @@ -104,10 +104,11 @@ func basicTestCase(t *testing.T) *resource.TestCase { Check: checksFlexCluster(projectID, clusterName, false, true), }, { - ResourceName: resourceName, - ImportStateIdFunc: acc.ImportStateIDFuncProjectIDClusterName(resourceName, "project_id", "name"), - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportStateIdFunc: acc.ImportStateIDFuncProjectIDClusterName(resourceName, "project_id", "name"), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"delete_on_create_timeout"}, }, }, } diff --git a/internal/service/project/resource_project.go b/internal/service/project/resource_project.go index bf14191f83..458ae655f6 100644 --- a/internal/service/project/resource_project.go +++ b/internal/service/project/resource_project.go @@ -362,7 +362,6 @@ func (r *projectRS) ImportState(ctx context.Context, req resource.ImportStateReq func updatePlanFromConfig(projectPlanNewPtr, projectPlan *TFProjectRSModel) { // we need to reset defaults from what was previously in the state: // https://discuss.hashicorp.com/t/boolean-optional-default-value-migration-to-framework/55932 - projectPlanNewPtr.WithDefaultAlertsSettings = projectPlan.WithDefaultAlertsSettings projectPlanNewPtr.ProjectOwnerID = projectPlan.ProjectOwnerID if projectPlan.Tags.IsNull() && len(projectPlanNewPtr.Tags.Elements()) == 0 { projectPlanNewPtr.Tags = types.MapNull(types.StringType) diff --git a/internal/service/project/resource_project_migration_test.go b/internal/service/project/resource_project_migration_test.go index d3e3f765e9..cec2d453bf 100644 --- a/internal/service/project/resource_project_migration_test.go +++ b/internal/service/project/resource_project_migration_test.go @@ -79,15 +79,24 @@ func TestMigProject_withTeams(t *testing.T) { }) } -func TestMigProject_withFalseDefaultSettings(t *testing.T) { +// empty is tested by the TestMigProject_basic +func TestMigProject_withFalseDefaultAlertSettings(t *testing.T) { + resource.ParallelTest(t, *defaultAlertSettingsTestCase(t, false)) +} + +func TestMigProject_withTrueDefaultAlertSettings(t *testing.T) { + resource.ParallelTest(t, *defaultAlertSettingsTestCase(t, true)) +} + +func defaultAlertSettingsTestCase(t *testing.T, withDefaultAlertSettings bool) *resource.TestCase { + t.Helper() var ( orgID = os.Getenv("MONGODB_ATLAS_ORG_ID") projectOwnerID = os.Getenv("MONGODB_ATLAS_PROJECT_OWNER_ID") projectName = acc.RandomProjectName() - config = configWithFalseDefaultSettings(orgID, projectName, projectOwnerID) + config = configWithDefaultAlertSettings(orgID, projectName, projectOwnerID, withDefaultAlertSettings) ) - - resource.Test(t, resource.TestCase{ + return &resource.TestCase{ PreCheck: func() { mig.PreCheckBasicOwnerID(t) }, CheckDestroy: acc.CheckDestroyProject, Steps: []resource.TestStep{ @@ -102,7 +111,7 @@ func TestMigProject_withFalseDefaultSettings(t *testing.T) { }, mig.TestStepCheckEmptyPlan(config), }, - }) + } } func TestMigProject_withLimits(t *testing.T) { diff --git a/internal/service/project/resource_project_schema.go b/internal/service/project/resource_project_schema.go index de7b2f9a5c..ec1b482a12 100644 --- a/internal/service/project/resource_project_schema.go +++ b/internal/service/project/resource_project_schema.go @@ -8,7 +8,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" @@ -18,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/constant" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier" ) func ResourceSchema(ctx context.Context) schema.Schema { @@ -52,13 +52,16 @@ func ResourceSchema(ctx context.Context) schema.Schema { }, "project_owner_id": schema.StringAttribute{ Optional: true, + PlanModifiers: []planmodifier.String{ + customplanmodifier.CreateOnlyAttributePlanModifier(), + }, }, "with_default_alerts_settings": schema.BoolAttribute{ // Default values also must be Computed otherwise Terraform throws error: - // Schema Using Attribute Default For Non-Computed Attribute - Optional: true, - Computed: true, - Default: booldefault.StaticBool(true), + // Provider produced invalid plan: planned an invalid value for a non-computed attribute. + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{customplanmodifier.CreateOnlyAttributePlanModifierWithBoolDefault(true)}, }, "is_collect_database_specifics_statistics_enabled": schema.BoolAttribute{ Computed: true, diff --git a/internal/service/project/resource_project_test.go b/internal/service/project/resource_project_test.go index 685b6994be..af4fc6f730 100644 --- a/internal/service/project/resource_project_test.go +++ b/internal/service/project/resource_project_test.go @@ -665,9 +665,21 @@ func TestAccGovProject_withProjectOwner(t *testing.T) { func TestAccProject_withFalseDefaultSettings(t *testing.T) { var ( - orgID = os.Getenv("MONGODB_ATLAS_ORG_ID") - projectOwnerID = os.Getenv("MONGODB_ATLAS_PROJECT_OWNER_ID") - projectName = acc.RandomProjectName() + orgID = os.Getenv("MONGODB_ATLAS_ORG_ID") + projectOwnerID = os.Getenv("MONGODB_ATLAS_PROJECT_OWNER_ID") + projectName = acc.RandomProjectName() + importResourceName = resourceName + "2" + alertSettingsFalse = configWithDefaultAlertSettings(orgID, projectName, projectOwnerID, false) + alertSettingsTrue = configWithDefaultAlertSettings(orgID, projectName, projectOwnerID, true) + alertSettingsAbsent = configBasic(orgID, projectName, "", false, nil, nil) + // To test plan behavior after import it is necessary to use a different resource name, otherwise we get: + // Terraform is already managing a remote object for mongodbatlas_project.test. To import to this address you must first remove the existing object from the state. + // This happens because `ImportStatePersist` uses the previous WorkingDirectory where the state from previous steps are saved + // resource "mongodbatlas_project" "test" --> resource "mongodbatlas_project" "test2" + alertSettingsFalseImport = strings.Replace(alertSettingsFalse, "test", "test2", 1) + // Need BOTH mongodbatlas_project.test and mongodbatlas_project.test2, otherwise we get: + // expected empty plan, but mongodbatlas_project.test has planned action(s): [delete] + alertSettingsAbsentImport = alertSettingsFalse + strings.Replace(alertSettingsAbsent, "test", "test2", 1) ) resource.ParallelTest(t, resource.TestCase{ @@ -676,13 +688,25 @@ func TestAccProject_withFalseDefaultSettings(t *testing.T) { CheckDestroy: acc.CheckDestroyProject, Steps: []resource.TestStep{ { - Config: configWithFalseDefaultSettings(orgID, projectName, projectOwnerID), + Config: alertSettingsFalse, Check: resource.ComposeAggregateTestCheckFunc( checkExists(resourceName), resource.TestCheckResourceAttr(resourceName, "name", projectName), resource.TestCheckResourceAttr(resourceName, "org_id", orgID), ), }, + { + Config: alertSettingsTrue, + ExpectError: regexp.MustCompile("with_default_alerts_settings cannot be updated or set after import, remove it from the configuration or use the state value"), + }, + { + Config: alertSettingsFalseImport, + ResourceName: importResourceName, + ImportStateIdFunc: acc.ImportStateProjectIDFunc(resourceName), + ImportState: true, + ImportStatePersist: true, // save the state to use it in the next plan + }, + acc.TestStepCheckEmptyPlan(alertSettingsAbsentImport), }, }) } @@ -706,7 +730,7 @@ func TestAccProject_withUpdatedSettings(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", projectName), resource.TestCheckResourceAttr(resourceName, "org_id", orgID), resource.TestCheckResourceAttr(resourceName, "project_owner_id", projectOwnerID), - resource.TestCheckResourceAttr(resourceName, "with_default_alerts_settings", "false"), + resource.TestCheckResourceAttr(resourceName, "with_default_alerts_settings", "true"), // uses default value resource.TestCheckResourceAttr(resourceName, "is_collect_database_specifics_statistics_enabled", "false"), resource.TestCheckResourceAttr(resourceName, "is_data_explorer_enabled", "false"), resource.TestCheckResourceAttr(resourceName, "is_extended_storage_sizes_enabled", "false"), @@ -719,7 +743,7 @@ func TestAccProject_withUpdatedSettings(t *testing.T) { Config: acc.ConfigProjectWithSettings(projectName, orgID, projectOwnerID, true), Check: resource.ComposeAggregateTestCheckFunc( checkExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "with_default_alerts_settings", "true"), + resource.TestCheckResourceAttr(resourceName, "with_default_alerts_settings", "true"), // uses default value resource.TestCheckResourceAttr(resourceName, "is_collect_database_specifics_statistics_enabled", "true"), resource.TestCheckResourceAttr(resourceName, "is_data_explorer_enabled", "true"), resource.TestCheckResourceAttr(resourceName, "is_extended_storage_sizes_enabled", "true"), @@ -732,7 +756,7 @@ func TestAccProject_withUpdatedSettings(t *testing.T) { Config: acc.ConfigProjectWithSettings(projectName, orgID, projectOwnerID, false), Check: resource.ComposeAggregateTestCheckFunc( checkExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "with_default_alerts_settings", "false"), + resource.TestCheckResourceAttr(resourceName, "with_default_alerts_settings", "true"), // uses default value resource.TestCheckResourceAttr(resourceName, "is_collect_database_specifics_statistics_enabled", "false"), resource.TestCheckResourceAttr(resourceName, "is_data_explorer_enabled", "false"), resource.TestCheckResourceAttr(resourceName, "is_extended_storage_sizes_enabled", "false"), @@ -1249,15 +1273,15 @@ func configGovWithOwner(orgID, projectName, projectOwnerID string) string { `, orgID, projectName, projectOwnerID) } -func configWithFalseDefaultSettings(orgID, projectName, projectOwnerID string) string { +func configWithDefaultAlertSettings(orgID, projectName, projectOwnerID string, withDefaultAlertsSettings bool) string { return fmt.Sprintf(` resource "mongodbatlas_project" "test" { org_id = %[1]q name = %[2]q project_owner_id = %[3]q - with_default_alerts_settings = false + with_default_alerts_settings = %[4]t } - `, orgID, projectName, projectOwnerID) + `, orgID, projectName, projectOwnerID, withDefaultAlertsSettings) } func configWithLimits(orgID, projectName string, limits []*admin.DataFederationLimit) string { diff --git a/internal/service/pushbasedlogexport/resource_schema.go b/internal/service/pushbasedlogexport/resource_schema.go index f809a80b7d..728a7e5836 100644 --- a/internal/service/pushbasedlogexport/resource_schema.go +++ b/internal/service/pushbasedlogexport/resource_schema.go @@ -59,7 +59,7 @@ func ResourceSchema(ctx context.Context) schema.Schema { "delete_on_create_timeout": schema.BoolAttribute{ Optional: true, PlanModifiers: []planmodifier.Bool{ - customplanmodifier.CreateOnlyBoolPlanModifier(), + customplanmodifier.CreateOnlyAttributePlanModifier(), }, MarkdownDescription: "Indicates whether to delete the resource being created if a timeout is reached when waiting for completion. When set to `true` and timeout occurs, it triggers the deletion and returns immediately without waiting for deletion to complete. When set to `false`, the timeout will not trigger resource deletion. If you suspect a transient error when the value is `true`, wait before retrying to allow resource deletion to finish. Default is `true`.", }, diff --git a/internal/service/streamprocessor/resource_schema.go b/internal/service/streamprocessor/resource_schema.go index a180be509a..729403b219 100644 --- a/internal/service/streamprocessor/resource_schema.go +++ b/internal/service/streamprocessor/resource_schema.go @@ -81,7 +81,7 @@ func ResourceSchema(ctx context.Context) schema.Schema { "delete_on_create_timeout": schema.BoolAttribute{ Optional: true, PlanModifiers: []planmodifier.Bool{ - customplanmodifier.CreateOnlyBoolPlanModifier(), + customplanmodifier.CreateOnlyAttributePlanModifier(), }, MarkdownDescription: "Indicates whether to delete the resource being created if a timeout is reached when waiting for completion. When set to `true` and timeout occurs, it triggers the deletion and returns immediately without waiting for deletion to complete. When set to `false`, the timeout will not trigger resource deletion. If you suspect a transient error when the value is `true`, wait before retrying to allow resource deletion to finish. Default is `true`.", }, diff --git a/internal/testutil/acc/pre_check.go b/internal/testutil/acc/pre_check.go index 91016e62ac..d6acde1226 100644 --- a/internal/testutil/acc/pre_check.go +++ b/internal/testutil/acc/pre_check.go @@ -140,13 +140,20 @@ func PreCheckPublicKey2(tb testing.TB) { } } -func PreCheckGPCEnv(tb testing.TB) { +func PreCheckGCPEnv(tb testing.TB) { tb.Helper() if os.Getenv("GCP_SERVICE_ACCOUNT_KEY") == "" || os.Getenv("GCP_KEY_VERSION_RESOURCE_ID") == "" { tb.Fatal("`GCP_SERVICE_ACCOUNT_KEY` and `GCP_KEY_VERSION_RESOURCE_ID` must be set for acceptance testing") } } +func PreCheckGCPEnvWithRole(tb testing.TB) { + tb.Helper() + if os.Getenv("GCP_ROLE_ID") == "" || os.Getenv("GCP_KEY_VERSION_RESOURCE_ID") == "" { + tb.Fatal("`GCP_ROLE_ID` and `GCP_KEY_VERSION_RESOURCE_ID` must be set for acceptance testing") + } +} + func PreCheckPeeringEnvAWS(tb testing.TB) { tb.Helper() if os.Getenv("AWS_ACCOUNT_ID") == "" || diff --git a/internal/testutil/acc/project.go b/internal/testutil/acc/project.go index e893d7cd9b..e338c6b1f0 100644 --- a/internal/testutil/acc/project.go +++ b/internal/testutil/acc/project.go @@ -36,7 +36,6 @@ func ConfigProjectWithSettings(projectName, orgID, projectOwnerID string, value name = %[1]q org_id = %[2]q project_owner_id = %[3]q - with_default_alerts_settings = %[4]t is_collect_database_specifics_statistics_enabled = %[4]t is_data_explorer_enabled = %[4]t is_extended_storage_sizes_enabled = %[4]t diff --git a/templates/data-sources/encryption_at_rest.md.tmpl b/templates/data-sources/encryption_at_rest.md.tmpl index 42aeab99ec..d05cb7087f 100644 --- a/templates/data-sources/encryption_at_rest.md.tmpl +++ b/templates/data-sources/encryption_at_rest.md.tmpl @@ -29,6 +29,8 @@ subcategory: "Encryption at Rest using Customer Key Management" -> **NOTE:** It is possible to configure Atlas Encryption at Rest to communicate with Customer Managed Keys (Azure Key Vault or AWS KMS) over private network interfaces (Azure Private Link or AWS PrivateLink). This requires enabling the `azure_key_vault_config.require_private_networking` or the `aws_kms_config.require_private_networking` attribute, together with the configuration of the `mongodbatlas_encryption_at_rest_private_endpoint` resource. Please review the `mongodbatlas_encryption_at_rest_private_endpoint` resource for details. ### Configuring encryption at rest using customer key management in GCP +For authentication, you must provide either serviceAccountKey (static credentials) or roleId (service-account–based authentication). Once roleId is configured, serviceAccountKey is no longer supported. + ```terraform resource "mongodbatlas_encryption_at_rest" "test" { project_id = var.atlas_project_id diff --git a/templates/resources/encryption_at_rest.md.tmpl b/templates/resources/encryption_at_rest.md.tmpl index ec4301563d..495ec7cd2c 100644 --- a/templates/resources/encryption_at_rest.md.tmpl +++ b/templates/resources/encryption_at_rest.md.tmpl @@ -57,6 +57,8 @@ Please review the [`mongodbatlas_encryption_at_rest_private_endpoint` resource d ### Configuring encryption at rest using customer key management in GCP +For authentication, you must provide either serviceAccountKey (static credentials) or roleId (service-account–based authentication). Once roleId is configured, serviceAccountKey is no longer supported. + ```terraform resource "mongodbatlas_encryption_at_rest" "test" { project_id = var.atlas_project_id