From 6c5a580b9f151685859652a0cbadbfc2c79cdd37 Mon Sep 17 00:00:00 2001 From: Jiawei Tao Date: Wed, 22 Feb 2023 21:16:09 +0800 Subject: [PATCH] service connector: add support for secret_store --- internal/services/serviceconnector/helper.go | 46 +++++++ .../service_connector_app_service_resource.go | 28 ++++- ...ice_connector_app_service_resource_test.go | 116 ++++++++++++++++++ ...service_connector_spring_cloud_resource.go | 28 ++++- ...ce_connector_spring_cloud_resource_test.go | 88 +++++++++++++ .../r/app_service_connection.html.markdown | 8 ++ .../r/spring_cloud_connection.html.markdown | 9 ++ 7 files changed, 311 insertions(+), 12 deletions(-) diff --git a/internal/services/serviceconnector/helper.go b/internal/services/serviceconnector/helper.go index 68dbb4029cbc..9692a73e70f1 100644 --- a/internal/services/serviceconnector/helper.go +++ b/internal/services/serviceconnector/helper.go @@ -21,6 +21,27 @@ type AuthInfoModel struct { Certificate string `tfschema:"certificate"` } +type SecretStoreModel struct { + KeyVaultId string `tfschema:"key_vault_id"` +} + +func secretStoreSchema() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*schema.Schema{ + "key_vault_id": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + } +} + func authInfoSchema() *pluginsdk.Schema { return &pluginsdk.Schema{ Type: pluginsdk.TypeList, @@ -83,6 +104,18 @@ func authInfoSchema() *pluginsdk.Schema { } } +func expandSecretStore(input []SecretStoreModel) *servicelinker.SecretStore { + if len(input) == 0 { + return nil + } + v := input[0] + + keyVaultId := v.KeyVaultId + return &servicelinker.SecretStore{ + KeyVaultId: utils.String(keyVaultId), + } +} + func expandServiceConnectorAuthInfo(input []AuthInfoModel) (servicelinker.AuthInfoBase, error) { if len(input) == 0 { return nil, fmt.Errorf("authentication should be defined") @@ -286,3 +319,16 @@ func flattenTargetService(input servicelinker.TargetServiceBase) string { return targetServiceId } + +func flattenSecretStore(input servicelinker.SecretStore) []SecretStoreModel { + var keyVaultId string + if input.KeyVaultId != nil { + keyVaultId = *input.KeyVaultId + } + + return []SecretStoreModel{ + { + KeyVaultId: keyVaultId, + }, + } +} diff --git a/internal/services/serviceconnector/service_connector_app_service_resource.go b/internal/services/serviceconnector/service_connector_app_service_resource.go index fa0aff77904b..855356052560 100644 --- a/internal/services/serviceconnector/service_connector_app_service_resource.go +++ b/internal/services/serviceconnector/service_connector_app_service_resource.go @@ -21,12 +21,13 @@ import ( type AppServiceConnectorResource struct{} type AppServiceConnectorResourceModel struct { - Name string `tfschema:"name"` - AppServiceId string `tfschema:"app_service_id"` - TargetResourceId string `tfschema:"target_resource_id"` - ClientType string `tfschema:"client_type"` - AuthInfo []AuthInfoModel `tfschema:"authentication"` - VnetSolution string `tfschema:"vnet_solution"` + Name string `tfschema:"name"` + AppServiceId string `tfschema:"app_service_id"` + TargetResourceId string `tfschema:"target_resource_id"` + ClientType string `tfschema:"client_type"` + AuthInfo []AuthInfoModel `tfschema:"authentication"` + VnetSolution string `tfschema:"vnet_solution"` + SecretStore []SecretStoreModel `tfschema:"secret_store"` } func (r AppServiceConnectorResource) Arguments() map[string]*schema.Schema { @@ -70,6 +71,8 @@ func (r AppServiceConnectorResource) Arguments() map[string]*schema.Schema { }, false), }, + "secret_store": secretStoreSchema(), + "vnet_solution": { Type: pluginsdk.TypeString, Optional: true, @@ -136,6 +139,11 @@ func (r AppServiceConnectorResource) Create() sdk.ResourceFunc { } } + if model.SecretStore != nil { + secretStore := expandSecretStore(model.SecretStore) + serviceConnectorProperties.SecretStore = secretStore + } + if model.ClientType != "" { clientType := servicelinker.ClientType(model.ClientType) serviceConnectorProperties.ClientType = &clientType @@ -206,6 +214,10 @@ func (r AppServiceConnectorResource) Read() sdk.ResourceFunc { state.VnetSolution = string(*props.VNetSolution.Type) } + if props.SecretStore != nil { + state.SecretStore = flattenSecretStore(*props.SecretStore) + } + return metadata.Encode(&state) } return nil @@ -265,6 +277,10 @@ func (r AppServiceConnectorResource) Update() sdk.ResourceFunc { linkerProps.VNetSolution = &vnetSolution } + if d.HasChange("secret_store") { + linkerProps.SecretStore = (*links.SecretStore)(expandSecretStore(state.SecretStore)) + } + if d.HasChange("authentication") { linkerProps.AuthInfo = state.AuthInfo } diff --git a/internal/services/serviceconnector/service_connector_app_service_resource_test.go b/internal/services/serviceconnector/service_connector_app_service_resource_test.go index 18f6c37a50e0..fa969734a72c 100644 --- a/internal/services/serviceconnector/service_connector_app_service_resource_test.go +++ b/internal/services/serviceconnector/service_connector_app_service_resource_test.go @@ -62,6 +62,21 @@ func TestAccServiceConnectorAppServiceStorageBlob_basic(t *testing.T) { }) } +func TestAccServiceConnectorAppServiceStorageBlob_secretStore(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_app_service_connection", "test") + r := ServiceConnectorAppServiceResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.secretStore(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func TestAccServiceConnectorAppServiceCosmosdb_update(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_app_service_connection", "test") r := ServiceConnectorAppServiceResource{} @@ -200,6 +215,107 @@ resource "azurerm_app_service_connection" "test" { `, template, data.RandomString, data.RandomInteger) } +func (r ServiceConnectorAppServiceResource) secretStore(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + key_vault { + recover_soft_deleted_key_vaults = false + purge_soft_delete_on_destroy = false + purge_soft_deleted_keys_on_destroy = false + } + } +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[2]d" + location = "%[1]s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestacc%[4]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_service_plan" "test" { + name = "acctestASP-%[2]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + os_type = "Linux" + sku_name = "P1v2" +} + +resource "azurerm_virtual_network" "test" { + name = "vnet-%[3]d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test1" { + name = "subnet1" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + private_endpoint_network_policies_enabled = true + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + + +resource "azurerm_linux_web_app" "test" { + name = "acctestWA-%[3]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + service_plan_id = azurerm_service_plan.test.id + virtual_network_subnet_id = azurerm_subnet.test1.id + + site_config {} + lifecycle { + ignore_changes = [ + app_settings["AZURE_STORAGEBLOB_RESOURCEENDPOINT"], + identity, + ] + } +} + +resource "azurerm_key_vault" "test" { + name = "accAKV-%[4]s" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + purge_protection_enabled = true +} + +resource "azurerm_app_service_connection" "test" { + name = "acctestserviceconnector%[3]d" + app_service_id = azurerm_linux_web_app.test.id + target_resource_id = azurerm_storage_account.test.id + client_type = "java" + + secret_store { + key_vault_id = azurerm_key_vault.test.id + } + authentication { + type = "systemAssignedIdentity" + } +} +`, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomString) +} + func (r ServiceConnectorAppServiceResource) complete(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { diff --git a/internal/services/serviceconnector/service_connector_spring_cloud_resource.go b/internal/services/serviceconnector/service_connector_spring_cloud_resource.go index 09727c020542..ae978ecdd933 100644 --- a/internal/services/serviceconnector/service_connector_spring_cloud_resource.go +++ b/internal/services/serviceconnector/service_connector_spring_cloud_resource.go @@ -21,12 +21,13 @@ import ( type SpringCloudConnectorResource struct{} type SpringCloudConnectorResourceModel struct { - Name string `tfschema:"name"` - SpringCloudId string `tfschema:"spring_cloud_id"` - TargetResourceId string `tfschema:"target_resource_id"` - ClientType string `tfschema:"client_type"` - AuthInfo []AuthInfoModel `tfschema:"authentication"` - VnetSolution string `tfschema:"vnet_solution"` + Name string `tfschema:"name"` + SpringCloudId string `tfschema:"spring_cloud_id"` + TargetResourceId string `tfschema:"target_resource_id"` + ClientType string `tfschema:"client_type"` + AuthInfo []AuthInfoModel `tfschema:"authentication"` + VnetSolution string `tfschema:"vnet_solution"` + SecretStore []SecretStoreModel `tfschema:"secret_store"` } func (r SpringCloudConnectorResource) Arguments() map[string]*schema.Schema { @@ -72,6 +73,8 @@ func (r SpringCloudConnectorResource) Arguments() map[string]*schema.Schema { }, false), }, + "secret_store": secretStoreSchema(), + "vnet_solution": { Type: pluginsdk.TypeString, Optional: true, @@ -138,6 +141,11 @@ func (r SpringCloudConnectorResource) Create() sdk.ResourceFunc { } } + if model.SecretStore != nil { + secretStore := expandSecretStore(model.SecretStore) + serviceConnectorProperties.SecretStore = secretStore + } + if model.ClientType != "" { clientType := servicelinker.ClientType(model.ClientType) serviceConnectorProperties.ClientType = &clientType @@ -207,6 +215,10 @@ func (r SpringCloudConnectorResource) Read() sdk.ResourceFunc { state.VnetSolution = string(*props.VNetSolution.Type) } + if props.SecretStore != nil { + state.SecretStore = flattenSecretStore(*props.SecretStore) + } + return metadata.Encode(&state) } return nil @@ -266,6 +278,10 @@ func (r SpringCloudConnectorResource) Update() sdk.ResourceFunc { linkerProps.VNetSolution = &vnetSolution } + if d.HasChange("secret_store") { + linkerProps.SecretStore = (*links.SecretStore)(expandSecretStore(state.SecretStore)) + } + if d.HasChange("authentication") { linkerProps.AuthInfo = state.AuthInfo } diff --git a/internal/services/serviceconnector/service_connector_spring_cloud_resource_test.go b/internal/services/serviceconnector/service_connector_spring_cloud_resource_test.go index 318eae803277..2cc27cccf59f 100644 --- a/internal/services/serviceconnector/service_connector_spring_cloud_resource_test.go +++ b/internal/services/serviceconnector/service_connector_spring_cloud_resource_test.go @@ -62,6 +62,21 @@ func TestAccServiceConnectorSpringCloudStorageBlob_basic(t *testing.T) { }) } +func TestAccServiceConnectorSpringCloudStorageBlob_secretStore(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_spring_cloud_connection", "test") + r := ServiceConnectorSpringCloudResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.secretStore(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func TestAccServiceConnectorSpringCloudCosmosdb_update(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_spring_cloud_connection", "test") r := ServiceConnectorSpringCloudResource{} @@ -217,6 +232,79 @@ resource "azurerm_spring_cloud_connection" "test" { `, template, data.RandomString, data.RandomInteger) } +func (r ServiceConnectorSpringCloudResource) secretStore(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + key_vault { + recover_soft_deleted_key_vaults = false + purge_soft_delete_on_destroy = false + purge_soft_deleted_keys_on_destroy = false + } + } +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[2]d" + location = "%[1]s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestacc%[3]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_key_vault" "test" { + name = "accAKV-%[3]s" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + purge_protection_enabled = true +} + +resource "azurerm_spring_cloud_service" "test" { + name = "testspringcloudservice-%[3]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_spring_cloud_app" "test" { + name = "testspringcloud-%[3]s" + resource_group_name = azurerm_resource_group.test.name + service_name = azurerm_spring_cloud_service.test.name + + identity { + type = "SystemAssigned" + } +} + +resource "azurerm_spring_cloud_java_deployment" "test" { + name = "deploy-%[3]s" + spring_cloud_app_id = azurerm_spring_cloud_app.test.id +} + + +resource "azurerm_spring_cloud_connection" "test" { + name = "acctestserviceconnector%[2]d" + spring_cloud_id = azurerm_spring_cloud_java_deployment.test.id + target_resource_id = azurerm_storage_account.test.id + + secret_store { + key_vault_id = azurerm_key_vault.test.id + } + authentication { + type = "systemAssignedIdentity" + } +} +`, data.Locations.Primary, data.RandomInteger, data.RandomString) +} + func (r ServiceConnectorSpringCloudResource) complete(data acceptance.TestData) string { template := r.template(data) return fmt.Sprintf(` diff --git a/website/docs/r/app_service_connection.html.markdown b/website/docs/r/app_service_connection.html.markdown index 0cc83676b479..bd23b1131810 100644 --- a/website/docs/r/app_service_connection.html.markdown +++ b/website/docs/r/app_service_connection.html.markdown @@ -114,6 +114,14 @@ An `authentication` block supports the following: * `vnet_solution` - (Optional) The type of the VNet solution. Possible values are `serviceEndpoint`, `privateLink`. +* `secret_store` - (Optional) An option to store secret value in secure place. An `secret_store` block as defined below. + +--- + +An `secret_store` block supports the following: + +* `key_vault_id` - (required) The key vault id to store secret. + ## Attribute Reference In addition to the Arguments listed above - the following Attributes are exported: diff --git a/website/docs/r/spring_cloud_connection.html.markdown b/website/docs/r/spring_cloud_connection.html.markdown index 60247e166547..f3d041a1e2ca 100644 --- a/website/docs/r/spring_cloud_connection.html.markdown +++ b/website/docs/r/spring_cloud_connection.html.markdown @@ -119,6 +119,15 @@ An `authentication` block supports the following: * `vnet_solution` - (Optional) The type of the VNet solution. Possible values are `serviceEndpoint`, `privateLink`. +* `secret_store` - (Optional) An option to store secret value in secure place. An `secret_store` block as defined below. + +--- + +An `secret_store` block supports the following: + +* `key_vault_id` - (required) The key vault id to store secret. + + ## Attribute Reference In addition to the Arguments listed above - the following Attributes are exported: