diff --git a/.changelog/3484.txt b/.changelog/3484.txt new file mode 100644 index 0000000000..fc8a06df40 --- /dev/null +++ b/.changelog/3484.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/mongodbatlas_federated_database_instance: Adds `azure` attribute to allow the creation of federated databases with Azure cloud provider configuration +``` + +```release-note:enhancement +data-source/mongodbatlas_federated_database_instance: Adds `azure` attribute to support reading federated databases with Azure cloud provider configuration +``` + +```release-note:enhancement +data-source/mongodbatlas_federated_database_instances: Adds `azure` attribute to support reading federated databases with Azure cloud provider configuration +``` \ No newline at end of file diff --git a/.github/workflows/acceptance-tests-runner.yml b/.github/workflows/acceptance-tests-runner.yml index 622a5dd3c5..a524283e3f 100644 --- a/.github/workflows/acceptance-tests-runner.yml +++ b/.github/workflows/acceptance-tests-runner.yml @@ -768,6 +768,9 @@ jobs: MONGODB_ATLAS_FEDERATED_ISSUER_URI: ${{ inputs.mongodb_atlas_federated_issuer_uri }} MONGODB_ATLAS_FEDERATED_ORG_ID: ${{ inputs.mongodb_atlas_federated_org_id }} MONGODB_ATLAS_FEDERATED_SETTINGS_ASSOCIATED_DOMAIN: ${{ inputs.mongodb_atlas_federated_settings_associated_domain }} + AZURE_ATLAS_APP_ID: ${{ inputs.azure_atlas_app_id }} + AZURE_SERVICE_PRINCIPAL_ID: ${{ inputs.azure_service_principal_id }} + AZURE_TENANT_ID: ${{ inputs.azure_tenant_id }} AWS_S3_BUCKET: ${{ secrets.aws_s3_bucket_federation }} AWS_REGION: ${{ inputs.aws_region_federation }} AWS_ACCESS_KEY_ID: ${{ secrets.aws_access_key_id }} 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 12ff15af53..33f7892fe3 100644 --- a/internal/service/cloudprovideraccess/data_source_cloud_provider_access_setup.go +++ b/internal/service/cloudprovideraccess/data_source_cloud_provider_access_setup.go @@ -22,7 +22,7 @@ func DataSourceSetup() *schema.Resource { "provider_name": { Type: schema.TypeString, Required: true, - ValidateFunc: validation.StringInSlice([]string{"AWS"}, false), + ValidateFunc: validation.StringInSlice([]string{"AWS", "AZURE"}, false), }, "role_id": { Type: schema.TypeString, 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 29023957ef..824cdcad55 100644 --- a/internal/service/cloudprovideraccess/resource_cloud_provider_access_setup_test.go +++ b/internal/service/cloudprovideraccess/resource_cloud_provider_access_setup_test.go @@ -17,6 +17,16 @@ func TestAccCloudProviderAccessSetupAWS_basic(t *testing.T) { resource.ParallelTest(t, *basicSetupTestCase(t)) } +const ( + cloudProviderAzureDataSource = ` + data "mongodbatlas_cloud_provider_access_setup" "test" { + project_id = mongodbatlas_cloud_provider_access_setup.test.project_id + provider_name = "AZURE" + role_id = mongodbatlas_cloud_provider_access_setup.test.role_id + } + ` +) + func TestAccCloudProviderAccessSetupAzure_basic(t *testing.T) { var ( resourceName = "mongodbatlas_cloud_provider_access_setup.test" @@ -26,13 +36,12 @@ func TestAccCloudProviderAccessSetupAzure_basic(t *testing.T) { tenantID = os.Getenv("AZURE_TENANT_ID") projectID = acc.ProjectIDExecution(t) ) - resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acc.PreCheckCloudProviderAccessAzure(t) }, ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, Steps: []resource.TestStep{ { - Config: configSetupAzure(projectID, atlasAzureAppID, servicePrincipalID, tenantID), + Config: acc.ConfigSetupAzure(projectID, atlasAzureAppID, servicePrincipalID, tenantID) + cloudProviderAzureDataSource, Check: resource.ComposeAggregateTestCheckFunc( checkExists(resourceName), resource.TestCheckResourceAttrSet(resourceName, "role_id"), @@ -104,26 +113,6 @@ func configSetupAWS(projectID string) string { `, projectID) } -func configSetupAzure(projectID, atlasAzureAppID, servicePrincipalID, tenantID string) string { - return fmt.Sprintf(` - resource "mongodbatlas_cloud_provider_access_setup" "test" { - project_id = %[1]q - provider_name = "AZURE" - azure_config { - atlas_azure_app_id = %[2]q - service_principal_id = %[3]q - tenant_id = %[4]q - } - } - - data "mongodbatlas_cloud_provider_access_setup" "test" { - project_id = mongodbatlas_cloud_provider_access_setup.test.project_id - provider_name = "AWS" - role_id = mongodbatlas_cloud_provider_access_setup.test.role_id - } - `, projectID, atlasAzureAppID, servicePrincipalID, tenantID) -} - func checkExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] diff --git a/internal/service/federateddatabaseinstance/data_source_federated_database_instance.go b/internal/service/federateddatabaseinstance/data_source_federated_database_instance.go index 89fe877339..5e6a1bfc16 100644 --- a/internal/service/federateddatabaseinstance/data_source_federated_database_instance.go +++ b/internal/service/federateddatabaseinstance/data_source_federated_database_instance.go @@ -33,47 +33,7 @@ func DataSource() *schema.Resource { Type: schema.TypeString, }, }, - "cloud_provider_config": { - Type: schema.TypeList, - MaxItems: 1, - Computed: true, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "aws": { - Type: schema.TypeList, - MaxItems: 1, - Computed: true, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "role_id": { - Type: schema.TypeString, - Computed: true, - }, - "test_s3_bucket": { - Type: schema.TypeString, - Computed: true, - Optional: true, - }, - "iam_assumed_role_arn": { - Type: schema.TypeString, - Computed: true, - }, - "iam_user_arn": { - Type: schema.TypeString, - Computed: true, - }, - "external_id": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, - }, - }, - }, + "cloud_provider_config": cloudProviderConfig(true), "data_process_region": { Type: schema.TypeList, Computed: true, diff --git a/internal/service/federateddatabaseinstance/data_source_federated_database_instances.go b/internal/service/federateddatabaseinstance/data_source_federated_database_instances.go index 64bb70d683..ec8b992cd4 100644 --- a/internal/service/federateddatabaseinstance/data_source_federated_database_instances.go +++ b/internal/service/federateddatabaseinstance/data_source_federated_database_instances.go @@ -45,47 +45,7 @@ func PluralDataSource() *schema.Resource { Type: schema.TypeString, }, }, - "cloud_provider_config": { - Type: schema.TypeList, - MaxItems: 1, - Computed: true, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "aws": { - Type: schema.TypeList, - MaxItems: 1, - Computed: true, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "role_id": { - Type: schema.TypeString, - Computed: true, - }, - "test_s3_bucket": { - Type: schema.TypeString, - Computed: true, - Optional: true, - }, - "iam_assumed_role_arn": { - Type: schema.TypeString, - Computed: true, - }, - "iam_user_arn": { - Type: schema.TypeString, - Computed: true, - }, - "external_id": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, - }, - }, - }, + "cloud_provider_config": cloudProviderConfig(true), "data_process_region": { Type: schema.TypeList, Computed: true, diff --git a/internal/service/federateddatabaseinstance/resource_federated_database_instance.go b/internal/service/federateddatabaseinstance/resource_federated_database_instance.go index 84f723fc90..012835026d 100644 --- a/internal/service/federateddatabaseinstance/resource_federated_database_instance.go +++ b/internal/service/federateddatabaseinstance/resource_federated_database_instance.go @@ -53,45 +53,8 @@ func Resource() *schema.Resource { Type: schema.TypeString, }, }, - "cloud_provider_config": { - Type: schema.TypeList, - MaxItems: 1, - Computed: true, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "aws": { - Type: schema.TypeList, - MaxItems: 1, - Required: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "role_id": { - Type: schema.TypeString, - Required: true, - }, - "test_s3_bucket": { - Type: schema.TypeString, - Required: true, - }, - "iam_assumed_role_arn": { - Type: schema.TypeString, - Computed: true, - }, - "iam_user_arn": { - Type: schema.TypeString, - Computed: true, - }, - "external_id": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, - }, - }, - }, + // Optional-only behavior from the API, but keeping O+C to avoid behavior changes. + "cloud_provider_config": cloudProviderConfig(false), "data_process_region": { Type: schema.TypeList, MaxItems: 1, @@ -708,7 +671,8 @@ func newUrls(urlsFromConfig []any) *[]string { func newCloudProviderConfig(d *schema.ResourceData) *admin.DataLakeCloudProviderConfig { if cloudProvider, ok := d.Get("cloud_provider_config").([]any); ok && len(cloudProvider) == 1 { return &admin.DataLakeCloudProviderConfig{ - Aws: newAWSConfig(cloudProvider), + Aws: newAWSConfig(cloudProvider), + Azure: newAzureConfig(cloudProvider), } } @@ -724,6 +688,14 @@ func newAWSConfig(cloudProvider []any) *admin.DataLakeAWSCloudProviderConfig { return nil } +func newAzureConfig(cloudProvider []any) *admin.DataFederationAzureCloudProviderConfig { + if azure, ok := cloudProvider[0].(map[string]any)["azure"].([]any); ok && len(azure) == 1 { + azureSchema := azure[0].(map[string]any) + return admin.NewDataFederationAzureCloudProviderConfig(azureSchema["role_id"].(string)) + } + return nil +} + func newDataProcessRegion(d *schema.ResourceData) *admin.DataLakeDataProcessRegion { if dataProcessRegion, ok := d.Get("data_process_region").([]any); ok && len(dataProcessRegion) == 1 { return &admin.DataLakeDataProcessRegion{ @@ -740,8 +712,18 @@ func flattenCloudProviderConfig(d *schema.ResourceData, cloudProviderConfig *adm return nil } - aws := cloudProviderConfig.GetAws() + return []map[string]any{ + { + "aws": flattenAWSCloudProviderConfig(d, cloudProviderConfig.Aws), + "azure": flattenAzureCloudProviderConfig(cloudProviderConfig.Azure), + }, + } +} +func flattenAWSCloudProviderConfig(d *schema.ResourceData, aws *admin.DataLakeAWSCloudProviderConfig) []map[string]any { + if aws == nil { + return nil + } awsOut := []map[string]any{ { "role_id": aws.GetRoleId(), @@ -751,22 +733,11 @@ func flattenCloudProviderConfig(d *schema.ResourceData, cloudProviderConfig *adm }, } - currentCloudProviderConfig, ok := d.Get("cloud_provider_config").([]any) - if !ok || len(currentCloudProviderConfig) == 0 { - return []map[string]any{ - { - "aws": &awsOut, - }, - } - } - // test_s3_bucket is not part of the API response - if currentAWS, ok := currentCloudProviderConfig[0].(map[string]any)["aws"].([]any); ok { - if testS3Bucket, ok := currentAWS[0].(map[string]any)["test_s3_bucket"].(string); ok { - awsOut[0]["test_s3_bucket"] = testS3Bucket - return []map[string]any{ - { - "aws": &awsOut, - }, + // Optionally add test_s3_bucket if present in the config + if currentCloudProviderConfig, ok := d.Get("cloud_provider_config").([]any); ok && len(currentCloudProviderConfig) > 0 { + if currentAWS, ok := currentCloudProviderConfig[0].(map[string]any)["aws"].([]any); ok && len(currentAWS) > 0 { + if testS3Bucket, ok := currentAWS[0].(map[string]any)["test_s3_bucket"].(string); ok && testS3Bucket != "" { + awsOut[0]["test_s3_bucket"] = testS3Bucket } } } @@ -774,6 +745,21 @@ func flattenCloudProviderConfig(d *schema.ResourceData, cloudProviderConfig *adm return awsOut } +func flattenAzureCloudProviderConfig(azure *admin.DataFederationAzureCloudProviderConfig) []map[string]any { + if azure == nil { + return nil + } + + return []map[string]any{ + { + "role_id": azure.GetRoleId(), + "atlas_app_id": azure.GetAtlasAppId(), + "service_principal_id": azure.GetServicePrincipalId(), + "tenant_id": azure.GetTenantId(), + }, + } +} + func flattenDataProcessRegion(processRegion *admin.DataLakeDataProcessRegion) []map[string]any { if processRegion == nil || (processRegion.Region == "" && processRegion.CloudProvider == "") { return nil diff --git a/internal/service/federateddatabaseinstance/resource_federated_database_instance_test.go b/internal/service/federateddatabaseinstance/resource_federated_database_instance_test.go index 0efd1174b0..d067692184 100644 --- a/internal/service/federateddatabaseinstance/resource_federated_database_instance_test.go +++ b/internal/service/federateddatabaseinstance/resource_federated_database_instance_test.go @@ -101,6 +101,42 @@ func TestAccFederatedDatabaseInstance_s3bucket(t *testing.T) { }) } +func TestAccFederatedDatabaseInstance_azureCloudProviderConfig(t *testing.T) { + var ( + resourceName = "mongodbatlas_federated_database_instance.test" + projectID = acc.ProjectIDExecution(t) + name = acc.RandomName() + atlasAzureAppID = os.Getenv("AZURE_ATLAS_APP_ID") + servicePrincipalID = os.Getenv("AZURE_SERVICE_PRINCIPAL_ID") + tenantID = os.Getenv("AZURE_TENANT_ID") + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.PreCheckCloudProviderAccessAzure(t) }, + ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, + CheckDestroy: acc.CheckDestroyFederatedDatabaseInstance, + Steps: []resource.TestStep{ + { + Config: configAzureCloudProvider(name, projectID, atlasAzureAppID, servicePrincipalID, tenantID), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "project_id"), + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttrSet(resourceName, "cloud_provider_config.0.azure.0.role_id"), + resource.TestCheckResourceAttr(resourceName, "cloud_provider_config.0.azure.0.atlas_app_id", atlasAzureAppID), + resource.TestCheckResourceAttr(resourceName, "cloud_provider_config.0.azure.0.service_principal_id", servicePrincipalID), + resource.TestCheckResourceAttr(resourceName, "cloud_provider_config.0.azure.0.tenant_id", tenantID), + ), + }, + { + ResourceName: resourceName, + ImportStateIdFunc: importStateIDFunc(resourceName), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccFederatedDatabaseInstance_atlasCluster(t *testing.T) { var ( specs = []acc.ReplicationSpecRequest{ @@ -396,6 +432,47 @@ resource "mongodbatlas_federated_database_instance" "test" { `, name, testS3Bucket) } +func configAzureCloudProvider(name, projectID, atlasAzureAppID, servicePrincipalID, tenantID string) string { + azureCloudProviderAccess := acc.ConfigSetupAzure(projectID, atlasAzureAppID, servicePrincipalID, tenantID) + + return azureCloudProviderAccess + fmt.Sprintf(` + +resource "mongodbatlas_federated_database_instance" "test" { + name = %[1]q + project_id = %[2]q + + cloud_provider_config { + azure { + role_id = mongodbatlas_cloud_provider_access_setup.test.role_id + + } + } + + storage_stores { + name = "azure_store" + cluster_name = "azure_cluster" + project_id = %[2]q + provider = "atlas" + read_preference { + mode = "secondary" + } + } + + storage_databases { + name = "VirtualDatabase0" + collections { + name = "VirtualCollection0" + data_sources { + collection = "listingsAndReviews" + database = "sample_airbnb" + store_name = "azure_store" + } + } + } +} +`, name, projectID) +} + func configFirstSteps(federatedInstanceName, projectName, orgID string) string { return fmt.Sprintf(` diff --git a/internal/service/federateddatabaseinstance/resource_schema.go b/internal/service/federateddatabaseinstance/resource_schema.go new file mode 100644 index 0000000000..6b0f4ee0cc --- /dev/null +++ b/internal/service/federateddatabaseinstance/resource_schema.go @@ -0,0 +1,86 @@ +package federateddatabaseinstance + +import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + +func cloudProviderConfig(isDataSource bool) *schema.Schema { + var computed, optional, required bool + var maxItems int + if isDataSource { + computed = true + maxItems = 0 + } else { + required = true + optional = true + maxItems = 1 + } + + return &schema.Schema{ + Type: schema.TypeList, + MaxItems: maxItems, + Computed: true, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "aws": { + Type: schema.TypeList, + MaxItems: maxItems, + Optional: true, + Computed: computed, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "role_id": { + Type: schema.TypeString, + Required: required, + Computed: computed, + }, + "test_s3_bucket": { + Type: schema.TypeString, + Required: required, + Optional: isDataSource, + }, + "iam_assumed_role_arn": { + Type: schema.TypeString, + Computed: true, + }, + "iam_user_arn": { + Type: schema.TypeString, + Computed: true, + }, + "external_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "azure": { + Type: schema.TypeList, + MaxItems: maxItems, + Optional: optional, + Computed: computed, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "role_id": { + Type: schema.TypeString, + Required: required, + Computed: computed, + }, + "atlas_app_id": { + Type: schema.TypeString, + Computed: true, + }, + "service_principal_id": { + Type: schema.TypeString, + Computed: true, + }, + "tenant_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + } +} diff --git a/internal/testutil/acc/cloud_provider_access.go b/internal/testutil/acc/cloud_provider_access.go new file mode 100644 index 0000000000..aa126da94a --- /dev/null +++ b/internal/testutil/acc/cloud_provider_access.go @@ -0,0 +1,17 @@ +package acc + +import "fmt" + +func ConfigSetupAzure(projectID, atlasAzureAppID, servicePrincipalID, tenantID string) string { + return fmt.Sprintf(` + resource "mongodbatlas_cloud_provider_access_setup" "test" { + project_id = %[1]q + provider_name = "AZURE" + azure_config { + atlas_azure_app_id = %[2]q + service_principal_id = %[3]q + tenant_id = %[4]q + } + } + `, projectID, atlasAzureAppID, servicePrincipalID, tenantID) +}