diff --git a/.github/labeler-issue-triage.yml b/.github/labeler-issue-triage.yml index 3662c34e4bb7..8452395202a4 100644 --- a/.github/labeler-issue-triage.yml +++ b/.github/labeler-issue-triage.yml @@ -25,7 +25,7 @@ service/app-configuration: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_app_configuration((.|\n)*)###' service/app-service: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(app_service_source_control|function_app_|linux_function_app\W+|linux_function_app\W+|linux_function_app_slot\W+|linux_web_app\W+|linux_web_app\W+|linux_web_app_slot\W+|service_plan|source_control_token|web_app_|windows_function_app\W+|windows_function_app\W+|windows_function_app_slot\W+|windows_web_app\W+|windows_web_app\W+|windows_web_app_slot\W+)((.|\n)*)###' + - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(app_service_source_control|function_app_active_slot\W+|function_app_function\W+|function_app_hybrid_connection\W+|linux_function_app\W+|linux_function_app\W+|linux_function_app_slot\W+|linux_web_app\W+|linux_web_app\W+|linux_web_app_slot\W+|service_plan|source_control_token|web_app_|windows_function_app\W+|windows_function_app\W+|windows_function_app_slot\W+|windows_web_app\W+|windows_web_app\W+|windows_web_app_slot\W+)((.|\n)*)###' service/application-insights: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_application_insights((.|\n)*)###' @@ -301,7 +301,7 @@ service/service-bus: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_servicebus_((.|\n)*)###' service/service-connector: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(app_service_connection|spring_cloud_connection)((.|\n)*)###' + - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(app_service_connection|function_app_connection|spring_cloud_connection)((.|\n)*)###' service/service-fabric: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_service_fabric_cluster((.|\n)*)###' diff --git a/internal/services/serviceconnector/registration.go b/internal/services/serviceconnector/registration.go index 30e9505e2fff..c4d258c34e4c 100644 --- a/internal/services/serviceconnector/registration.go +++ b/internal/services/serviceconnector/registration.go @@ -27,6 +27,7 @@ func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ AppServiceConnectorResource{}, SpringCloudConnectorResource{}, + FunctionAppConnectorResource{}, } } diff --git a/internal/services/serviceconnector/service_connector_function_app_resource.go b/internal/services/serviceconnector/service_connector_function_app_resource.go new file mode 100644 index 000000000000..ddc4328be08f --- /dev/null +++ b/internal/services/serviceconnector/service_connector_function_app_resource.go @@ -0,0 +1,301 @@ +package serviceconnector + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/servicelinker/2022-05-01/links" + "github.com/hashicorp/go-azure-sdk/resource-manager/servicelinker/2022-05-01/servicelinker" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/web/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type FunctionAppConnectorResource struct{} + +type FunctionAppConnectorResourceModel struct { + Name string `tfschema:"name"` + FunctionAppId string `tfschema:"function_app_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 FunctionAppConnectorResource) Arguments() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "function_app_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.FunctionAppID, + }, + + "target_resource_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: azure.ValidateResourceID, + }, + + "client_type": { + Type: pluginsdk.TypeString, + Optional: true, + Default: string(servicelinker.ClientTypeNone), + ValidateFunc: validation.StringInSlice([]string{ + string(servicelinker.ClientTypeDotnet), + string(servicelinker.ClientTypeJava), + string(servicelinker.ClientTypePython), + string(servicelinker.ClientTypeGo), + string(servicelinker.ClientTypePhp), + string(servicelinker.ClientTypeRuby), + string(servicelinker.ClientTypeDjango), + string(servicelinker.ClientTypeNodejs), + string(servicelinker.ClientTypeSpringBoot), + }, false), + }, + + "secret_store": secretStoreSchema(), + + "vnet_solution": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(servicelinker.VNetSolutionTypeServiceEndpoint), + string(servicelinker.VNetSolutionTypePrivateLink), + }, false), + }, + + "authentication": authInfoSchema(), + } +} + +func (r FunctionAppConnectorResource) Attributes() map[string]*schema.Schema { + return map[string]*schema.Schema{} +} + +func (r FunctionAppConnectorResource) ModelObject() interface{} { + return &FunctionAppConnectorResourceModel{} +} + +func (r FunctionAppConnectorResource) ResourceType() string { + return "azurerm_function_app_connection" +} + +func (r FunctionAppConnectorResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var model FunctionAppConnectorResourceModel + if err := metadata.Decode(&model); err != nil { + return err + } + + client := metadata.Client.ServiceConnector.ServiceLinkerClient + + id := servicelinker.NewScopedLinkerID(model.FunctionAppId, model.Name) + existing, err := client.LinkerGet(ctx, id) + if err != nil && !response.WasNotFound(existing.HttpResponse) { + return fmt.Errorf("checking for presence of existing %s: %+v", id, err) + } + + if !response.WasNotFound(existing.HttpResponse) { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + + authInfo, err := expandServiceConnectorAuthInfo(model.AuthInfo) + if err != nil { + return fmt.Errorf("expanding `authentication`: %+v", err) + } + + serviceConnectorProperties := servicelinker.LinkerProperties{ + AuthInfo: authInfo, + } + + if _, err := commonids.ParseStorageAccountID(model.TargetResourceId); err == nil { + targetResourceId := model.TargetResourceId + "/blobServices/default" + serviceConnectorProperties.TargetService = servicelinker.AzureResource{ + Id: &targetResourceId, + } + } else { + serviceConnectorProperties.TargetService = servicelinker.AzureResource{ + Id: &model.TargetResourceId, + } + } + + if model.SecretStore != nil { + secretStore := expandSecretStore(model.SecretStore) + serviceConnectorProperties.SecretStore = secretStore + } + + if model.ClientType != "" { + clientType := servicelinker.ClientType(model.ClientType) + serviceConnectorProperties.ClientType = &clientType + } + + if model.VnetSolution != "" { + vNetSolutionType := servicelinker.VNetSolutionType(model.VnetSolution) + vNetSolution := servicelinker.VNetSolution{ + Type: &vNetSolutionType, + } + serviceConnectorProperties.VNetSolution = &vNetSolution + } + + props := servicelinker.LinkerResource{ + Id: utils.String(id.ID()), + Name: utils.String(model.Name), + Properties: serviceConnectorProperties, + } + + if err := client.LinkerCreateOrUpdateThenPoll(ctx, id, props); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + metadata.SetID(id) + return nil + }, + } +} + +func (r FunctionAppConnectorResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ServiceConnector.ServiceLinkerClient + id, err := servicelinker.ParseScopedLinkerID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + resp, err := client.LinkerGet(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("reading %s: %+v", id, err) + } + + pwd := metadata.ResourceData.Get("authentication.0.secret").(string) + + if model := resp.Model; model != nil { + props := model.Properties + if props.AuthInfo == nil || props.TargetService == nil { + return nil + } + + state := FunctionAppConnectorResourceModel{ + Name: id.LinkerName, + FunctionAppId: id.ResourceUri, + TargetResourceId: flattenTargetService(props.TargetService), + AuthInfo: flattenServiceConnectorAuthInfo(props.AuthInfo, pwd), + } + + if props.ClientType != nil { + state.ClientType = string(*props.ClientType) + } + + if props.VNetSolution != nil && props.VNetSolution.Type != nil { + state.VnetSolution = string(*props.VNetSolution.Type) + } + + if props.SecretStore != nil { + state.SecretStore = flattenSecretStore(*props.SecretStore) + } + + return metadata.Encode(&state) + } + return nil + }, + } +} + +func (r FunctionAppConnectorResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ServiceConnector.LinksClient + id, err := links.ParseScopedLinkerID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + metadata.Logger.Infof("deleting %s", *id) + + if err := client.LinkerDeleteThenPoll(ctx, *id); err != nil { + return fmt.Errorf("deleting %s: %+v", *id, err) + } + return nil + }, + } +} + +func (r FunctionAppConnectorResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ServiceConnector.LinksClient + id, err := links.ParseScopedLinkerID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + var state FunctionAppConnectorResourceModel + if err := metadata.Decode(&state); err != nil { + return fmt.Errorf("decoding state: %+v", err) + } + + linkerProps := links.LinkerProperties{} + d := metadata.ResourceData + + if d.HasChange("client_type") { + clientType := links.ClientType(state.ClientType) + linkerProps.ClientType = &clientType + } + + if d.HasChange("vnet_solution") { + vnetSolutionType := links.VNetSolutionType(state.VnetSolution) + vnetSolution := links.VNetSolution{ + Type: &vnetSolutionType, + } + linkerProps.VNetSolution = &vnetSolution + } + + if d.HasChange("secret_store") { + linkerProps.SecretStore = (*links.SecretStore)(expandSecretStore(state.SecretStore)) + } + + if d.HasChange("authentication") { + linkerProps.AuthInfo = state.AuthInfo + } + + props := links.LinkerPatch{ + Properties: &linkerProps, + } + + if err := client.LinkerUpdateThenPoll(ctx, *id, props); err != nil { + return fmt.Errorf("updating %s: %+v", id, err) + } + + return nil + }, + } +} + +func (r FunctionAppConnectorResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return servicelinker.ValidateScopedLinkerID +} diff --git a/internal/services/serviceconnector/service_connector_function_app_resource_test.go b/internal/services/serviceconnector/service_connector_function_app_resource_test.go new file mode 100644 index 000000000000..2f3e4821015c --- /dev/null +++ b/internal/services/serviceconnector/service_connector_function_app_resource_test.go @@ -0,0 +1,514 @@ +package serviceconnector_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/resource-manager/servicelinker/2022-05-01/servicelinker" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type FunctionAppConnectorResource struct{} + +func (r FunctionAppConnectorResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := servicelinker.ParseScopedLinkerID(state.ID) + if err != nil { + return nil, err + } + + resp, err := client.ServiceConnector.ServiceLinkerClient.LinkerGet(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return utils.Bool(false), nil + } + return nil, fmt.Errorf("retrieving %s: %+v", *id, err) + } + return utils.Bool(true), nil +} + +func TestAccServiceConnectorFunctionAppCosmosdb_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_connection", "test") + r := FunctionAppConnectorResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.cosmosdbBasic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccServiceConnectorFunctionAppCosmosdb_secretAuth(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_connection", "test") + r := FunctionAppConnectorResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.cosmosdbSecretAuth(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("authentication"), + }) +} + +func TestAccServiceConnectorFunctionAppCosmosdb_servicePrincipalSecretAuth(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_connection", "test") + r := FunctionAppConnectorResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.cosmosdbServicePrincipalSecretAuth(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("authentication"), + }) +} + +func TestAccServiceConnectorFunctionAppCosmosdb_userAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_connection", "test") + r := FunctionAppConnectorResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.cosmosdbWithUserAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("authentication"), + }) +} + +func TestAccServiceConnectorFunctionAppStorageBlob_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_connection", "test") + r := FunctionAppConnectorResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.storageBlob(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccServiceConnectorFunctionAppStorageBlob_secretStore(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_connection", "test") + r := FunctionAppConnectorResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.secretStore(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccServiceConnectorFunctionAppCosmosdb_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_connection", "test") + r := FunctionAppConnectorResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.cosmosdbBasic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.cosmosdbUpdate(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccServiceConnectorFunctionApp_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_connection", "test") + r := FunctionAppConnectorResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (r FunctionAppConnectorResource) storageBlob(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[3]d" + location = "%[1]s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestacc%[2]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_app_service_plan" "test" { + name = "acctestASP-%[3]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + sku { + tier = "Standard" + size = "S1" + } +} + +resource "azurerm_function_app" "test" { + name = "acctest-%[3]d-func" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + app_service_plan_id = azurerm_app_service_plan.test.id + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + lifecycle { + ignore_changes = [ + identity, + ] + } +} + +resource "azurerm_function_app_connection" "test" { + name = "acctestserviceconnector%[3]d" + function_app_id = azurerm_function_app.test.id + target_resource_id = azurerm_storage_account.test.id + authentication { + type = "systemAssignedIdentity" + } +} +`, data.Locations.Primary, data.RandomString, data.RandomInteger) +} + +func (r FunctionAppConnectorResource) 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_app_service_plan" "test" { + name = "acctestASP-%[2]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + sku { + tier = "Standard" + size = "S1" + } +} + +resource "azurerm_function_app" "test" { + name = "acctest-%[2]d-func" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + app_service_plan_id = azurerm_app_service_plan.test.id + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + lifecycle { + ignore_changes = [ + identity, + ] + } +} + +resource "azurerm_function_app_connection" "test" { + name = "acctestserviceconnector%[2]d" + function_app_id = azurerm_function_app.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 FunctionAppConnectorResource) complete(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_function_app_connection" "test" { + name = "acctestserviceconnector%[2]d" + function_app_id = azurerm_function_app.test.id + target_resource_id = azurerm_cosmosdb_account.test.id + client_type = "java" + authentication { + type = "systemAssignedIdentity" + } +} +`, template, data.RandomInteger) +} + +func (r FunctionAppConnectorResource) cosmosdbBasic(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_function_app_connection" "test" { + name = "acctestserviceconnector%[2]d" + function_app_id = azurerm_function_app.test.id + target_resource_id = azurerm_cosmosdb_account.test.id + authentication { + type = "systemAssignedIdentity" + } +} +`, template, data.RandomInteger) +} + +func (r FunctionAppConnectorResource) cosmosdbSecretAuth(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_function_app_connection" "test" { + name = "acctestserviceconnector%[2]d" + function_app_id = azurerm_function_app.test.id + target_resource_id = azurerm_cosmosdb_account.test.id + authentication { + type = "secret" + name = "foo" + secret = "bar" + } +} +`, template, data.RandomInteger) +} + +func (r FunctionAppConnectorResource) cosmosdbServicePrincipalSecretAuth(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_user_assigned_identity" "test" { + name = "acctest%[2]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_function_app_connection" "test" { + name = "acctestserviceconnector%[3]d" + function_app_id = azurerm_function_app.test.id + target_resource_id = azurerm_cosmosdb_account.test.id + authentication { + type = "servicePrincipalSecret" + client_id = "someclientid" + principal_id = azurerm_user_assigned_identity.test.principal_id + secret = "bar" + } +} +`, template, data.RandomString, data.RandomInteger) +} + +func (r FunctionAppConnectorResource) cosmosdbWithUserAssignedIdentity(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%[1]s + +data "azurerm_subscription" "test" {} + +resource "azurerm_user_assigned_identity" "test" { + name = "acctest%[2]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_function_app_connection" "test" { + name = "acctestserviceconnector%[3]d" + function_app_id = azurerm_function_app.test.id + target_resource_id = azurerm_cosmosdb_account.test.id + authentication { + type = "userAssignedIdentity" + subscription_id = data.azurerm_subscription.test.subscription_id + client_id = azurerm_user_assigned_identity.test.client_id + } +} +`, template, data.RandomString, data.RandomInteger) +} + +func (r FunctionAppConnectorResource) cosmosdbUpdate(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_cosmosdb_sql_database" "update" { + name = "cosmos-sql-db-update" + resource_group_name = azurerm_resource_group.test.name + account_name = azurerm_cosmosdb_account.test.name + throughput = 400 +} + +resource "azurerm_cosmosdb_sql_container" "update" { + name = "test-containerupdate%[3]s" + resource_group_name = azurerm_resource_group.test.name + account_name = azurerm_cosmosdb_account.test.name + database_name = azurerm_cosmosdb_sql_database.test.name + partition_key_path = "/definitionupdate" +} + +resource "azurerm_service_plan" "update" { + location = azurerm_resource_group.test.location + name = "testserviceplanupdate%[3]s" + resource_group_name = azurerm_resource_group.test.name + sku_name = "P1v2" + os_type = "Linux" +} + +resource "azurerm_function_app_connection" "test" { + name = "acctestserviceconnector%[2]d" + function_app_id = azurerm_function_app.test.id + target_resource_id = azurerm_cosmosdb_account.test.id + authentication { + type = "systemAssignedIdentity" + } +} +`, template, data.RandomInteger, data.RandomString) +} + +func (r FunctionAppConnectorResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +resource "azurerm_cosmosdb_account" "test" { + name = "acctestcosmosdb%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + offer_type = "Standard" + kind = "GlobalDocumentDB" + + consistency_policy { + consistency_level = "BoundedStaleness" + max_interval_in_seconds = 10 + max_staleness_prefix = 200 + } + + geo_location { + location = azurerm_resource_group.test.location + failover_priority = 0 + } +} + +resource "azurerm_cosmosdb_sql_database" "test" { + name = "cosmos-sql-db" + resource_group_name = azurerm_resource_group.test.name + account_name = azurerm_cosmosdb_account.test.name + throughput = 400 +} + +resource "azurerm_cosmosdb_sql_container" "test" { + name = "test-container%[3]s" + resource_group_name = azurerm_resource_group.test.name + account_name = azurerm_cosmosdb_account.test.name + database_name = azurerm_cosmosdb_sql_database.test.name + partition_key_path = "/definition" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%[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_app_service_plan" "test" { + name = "acctestASP-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + sku { + tier = "Standard" + size = "S1" + } +} + +resource "azurerm_function_app" "test" { + name = "acctest-%[1]d-func" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + app_service_plan_id = azurerm_app_service_plan.test.id + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + lifecycle { + ignore_changes = [ + identity, + ] + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} diff --git a/website/docs/r/function_app_connection.html.markdown b/website/docs/r/function_app_connection.html.markdown new file mode 100644 index 000000000000..dacab32be8b5 --- /dev/null +++ b/website/docs/r/function_app_connection.html.markdown @@ -0,0 +1,163 @@ +--- +subcategory: "App Service (Web Apps)" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_function_app_connection" +description: |- + Manages a service connector for function app. +--- + +# azurerm_function_app_connection + +Manages a service connector for function app. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_cosmosdb_account" "example" { + name = "example-cosmosdb-account" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + offer_type = "Standard" + kind = "GlobalDocumentDB" + + consistency_policy { + consistency_level = "BoundedStaleness" + max_interval_in_seconds = 10 + max_staleness_prefix = 200 + } + + geo_location { + location = azurerm_resource_group.example.location + failover_priority = 0 + } +} + +resource "azurerm_cosmosdb_sql_database" "example" { + name = "cosmos-sql-db" + resource_group_name = azurerm_cosmosdb_account.example.resource_group_name + account_name = azurerm_cosmosdb_account.example.name + throughput = 400 +} + +resource "azurerm_cosmosdb_sql_container" "example" { + name = "example-container" + resource_group_name = azurerm_cosmosdb_account.example.resource_group_name + account_name = azurerm_cosmosdb_account.example.name + database_name = azurerm_cosmosdb_sql_database.example.name + partition_key_path = "/definition" +} + +resource "azurerm_storage_account" "example" { + name = "examplestorageaccount" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_service_plan" "example" { + location = azurerm_resource_group.example.location + name = "example-serviceplan" + resource_group_name = azurerm_resource_group.example.name + sku_name = "P1v2" + os_type = "Linux" +} + +resource "azurerm_function_app" "test" { + name = "example-function-app" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + app_service_plan_id = azurerm_app_service_plan.test.id + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + lifecycle { + ignore_changes = [ + identity, + ] + } +} + +resource "azurerm_app_service_connection" "example" { + name = "example-serviceconnector" + function_app_id = azurerm_function_app.example.id + target_resource_id = azurerm_cosmosdb_account.test.id + authentication { + type = "systemAssignedIdentity" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the service connection. Changing this forces a new resource to be created. + +* `function_app_id` - (Required) The ID of the data source function app. Changing this forces a new resource to be created. + +* `target_resource_id` - (Required) The ID of the target resource. Changing this forces a new resource to be created. Possible target resources are `Postgres`, `PostgresFlexible`, `Mysql`, `Sql`, `Redis`, `RedisEnterprise`, `CosmosCassandra`, `CosmosGremlin`, `CosmosMongo`, `CosmosSql`, `CosmosTable`, `StorageBlob`, `StorageQueue`, `StorageFile`, `StorageTable`, `AppConfig`, `EventHub`, `ServiceBus`, `SignalR`, `WebPubSub`, `ConfluentKafka`. The integration guide can be found [here](https://learn.microsoft.com/en-us/azure/service-connector/how-to-integrate-postgres). + +* `authentication` - (Required) The authentication info. An `authentication` block as defined below. + +-> **Note:** If a Managed Identity is used, this will need to be configured on the App Service. + +--- + +An `authentication` block supports the following: + +* `type` - (Required) The authentication type. Possible values are `systemAssignedIdentity`, `userAssignedIdentity`, `servicePrincipalSecret`, `servicePrincipalCertificate`, `secret`. Changing this forces a new resource to be created. + +* `name` - (Optional) Username or account name for secret auth. `name` and `secret` should be either both specified or both not specified when `type` is set to `secret`. + +* `secret` - (Optional) Password or account key for secret auth. `secret` and `name` should be either both specified or both not specified when `type` is set to `secret`. + +* `client_id` - (Optional) Client ID for `userAssignedIdentity` or `servicePrincipal` auth. Should be specified when `type` is set to `servicePrincipalSecret` or `servicePrincipalCertificate`. When `type` is set to `userAssignedIdentity`, `client_id` and `subscription_id` should be either both specified or both not specified. + +* `subscription_id` - (Optional) Subscription ID for `userAssignedIdentity`. `subscription_id` and `client_id` should be either both specified or both not specified. + +* `principal_id` - (Optional) Principal ID for `servicePrincipal` auth. Should be specified when `type` is set to `servicePrincipalSecret` or `servicePrincipalCertificate`. + +* `certificate` - (Optional) Service principal certificate for `servicePrincipal` auth. Should be specified when `type` is set to `servicePrincipalCertificate`. + +--- + +* `client_type` - (Optional) The application client type. Possible values are `none`, `dotnet`, `java`, `python`, `go`, `php`, `ruby`, `django`, `nodejs` and `springBoot`. + +* `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: + +* `id` - The ID of the service connector. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Service Connector for app service. +* `read` - (Defaults to 5 minutes) Used when retrieving the Service Connector for app service. +* `update` - (Defaults to 30 minutes) Used when updating the Service Connector for app service. +* `delete` - (Defaults to 30 minutes) Used when deleting the Service Connector for app service. + +## Import + +Service Connector for app service can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_app_service_connection.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Web/sites/webapp/providers/Microsoft.ServiceLinker/linkers/serviceconnector1 +```