diff --git a/azurerm/helpers/azure/app_service.go b/azurerm/helpers/azure/app_service.go index 242d623d81b0..f53c0fc55549 100644 --- a/azurerm/helpers/azure/app_service.go +++ b/azurerm/helpers/azure/app_service.go @@ -1346,14 +1346,14 @@ func FlattenAppServiceIdentity(identity *web.ManagedServiceIdentity) []interface return make([]interface{}, 0) } - result := make(map[string]interface{}) - result["type"] = string(identity.Type) - + principalId := "" if identity.PrincipalID != nil { - result["principal_id"] = *identity.PrincipalID + principalId = *identity.PrincipalID } + + tenantId := "" if identity.TenantID != nil { - result["tenant_id"] = *identity.TenantID + tenantId = *identity.TenantID } identityIds := make([]string, 0) @@ -1362,9 +1362,15 @@ func FlattenAppServiceIdentity(identity *web.ManagedServiceIdentity) []interface identityIds = append(identityIds, key) } } - result["identity_ids"] = identityIds - return []interface{}{result} + return []interface{}{ + map[string]interface{}{ + "identity_ids": identityIds, + "principal_id": principalId, + "tenant_id": tenantId, + "type": string(identity.Type), + }, + } } func ExpandAppServiceSiteConfig(input interface{}) (*web.SiteConfig, error) { diff --git a/azurerm/internal/services/web/resource_arm_function_app.go b/azurerm/internal/services/web/resource_arm_function_app.go index fabc21ef00ef..66d0bfb185c8 100644 --- a/azurerm/internal/services/web/resource_arm_function_app.go +++ b/azurerm/internal/services/web/resource_arm_function_app.go @@ -133,32 +133,7 @@ func resourceArmFunctionApp() *schema.Resource { }, }, - "identity": { - Type: schema.TypeList, - Optional: true, - Computed: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "type": { - Type: schema.TypeString, - Required: true, - DiffSuppressFunc: suppress.CaseDifference, - ValidateFunc: validation.StringInSlice([]string{ - string(web.ManagedServiceIdentityTypeSystemAssigned), - }, true), - }, - "principal_id": { - Type: schema.TypeString, - Computed: true, - }, - "tenant_id": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, + "identity": azure.SchemaAppServiceIdentity(), "tags": tags.Schema(), @@ -339,10 +314,10 @@ func resourceArmFunctionAppCreate(d *schema.ResourceData, meta interface{}) erro }, } - if v, ok := d.GetOk("identity.0.type"); ok { - siteEnvelope.Identity = &web.ManagedServiceIdentity{ - Type: web.ManagedServiceIdentityType(v.(string)), - } + if _, ok := d.GetOk("identity"); ok { + appServiceIdentityRaw := d.Get("identity").([]interface{}) + appServiceIdentity := azure.ExpandAppServiceIdentity(appServiceIdentityRaw) + siteEnvelope.Identity = appServiceIdentity } createFuture, err := client.CreateOrUpdate(ctx, resourceGroup, name, siteEnvelope) @@ -423,19 +398,19 @@ func resourceArmFunctionAppUpdate(d *schema.ResourceData, meta interface{}) erro }, } - if v, ok := d.GetOk("identity.0.type"); ok { - siteEnvelope.Identity = &web.ManagedServiceIdentity{ - Type: web.ManagedServiceIdentityType(v.(string)), - } + if _, ok := d.GetOk("identity"); ok { + appServiceIdentityRaw := d.Get("identity").([]interface{}) + appServiceIdentity := azure.ExpandAppServiceIdentity(appServiceIdentityRaw) + siteEnvelope.Identity = appServiceIdentity } future, err := client.CreateOrUpdate(ctx, resGroup, name, siteEnvelope) if err != nil { - return err + return fmt.Errorf("Error updating Function App %q (Resource Group %q): %+v", name, resGroup, err) } if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { - return err + return fmt.Errorf("Error waiting for update of Function App %q (Resource Group %q): %+v", name, resGroup, err) } appSettings := expandFunctionAppAppSettings(d, appServiceTier) @@ -560,10 +535,6 @@ func resourceArmFunctionAppRead(d *schema.ResourceData, meta interface{}) error d.Set("client_affinity_enabled", props.ClientAffinityEnabled) } - if err = d.Set("identity", flattenFunctionAppIdentity(resp.Identity)); err != nil { - return err - } - appSettings := flattenAppServiceAppSettings(appSettingsResp.Properties) d.Set("storage_connection_string", appSettings["AzureWebJobsStorage"]) @@ -585,6 +556,11 @@ func resourceArmFunctionAppRead(d *schema.ResourceData, meta interface{}) error return err } + identity := azure.FlattenAppServiceIdentity(resp.Identity) + if err := d.Set("identity", identity); err != nil { + return fmt.Errorf("Error setting `identity`: %s", err) + } + configResp, err := client.GetConfiguration(ctx, resGroup, name) if err != nil { return fmt.Errorf("Error making Read request on AzureRM Function App Configuration %q: %+v", name, err) @@ -832,24 +808,6 @@ func flattenFunctionAppConnectionStrings(input map[string]*web.ConnStringValueTy return results } -func flattenFunctionAppIdentity(identity *web.ManagedServiceIdentity) interface{} { - if identity == nil { - return make([]interface{}, 0) - } - - result := make(map[string]interface{}) - result["type"] = string(identity.Type) - - if identity.PrincipalID != nil { - result["principal_id"] = *identity.PrincipalID - } - if identity.TenantID != nil { - result["tenant_id"] = *identity.TenantID - } - - return []interface{}{result} -} - func flattenFunctionAppSiteCredential(input *web.UserProperties) []interface{} { results := make([]interface{}, 0) result := make(map[string]interface{}) diff --git a/azurerm/internal/services/web/tests/resource_arm_function_app_test.go b/azurerm/internal/services/web/tests/resource_arm_function_app_test.go index 77bd3494a632..8a0cc0d8e57a 100644 --- a/azurerm/internal/services/web/tests/resource_arm_function_app_test.go +++ b/azurerm/internal/services/web/tests/resource_arm_function_app_test.go @@ -437,6 +437,40 @@ func TestAccAzureRMFunctionApp_updateIdentity(t *testing.T) { }) } +func TestAccAzureRMFunctionApp_userAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMFunctionAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMFunctionApp_userAssignedIdentity(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFunctionAppExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "identity.0.type", "UserAssigned"), + resource.TestCheckResourceAttr(data.ResourceName, "identity.0.identity_ids.#", "1"), + resource.TestCheckResourceAttr(data.ResourceName, "identity.0.principal_id", ""), + resource.TestCheckResourceAttr(data.ResourceName, "identity.0.tenant_id", ""), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMFunctionApp_userAssignedIdentityUpdated(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFunctionAppExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "identity.0.type", "UserAssigned"), + resource.TestCheckResourceAttr(data.ResourceName, "identity.0.identity_ids.#", "2"), + resource.TestCheckResourceAttr(data.ResourceName, "identity.0.principal_id", ""), + resource.TestCheckResourceAttr(data.ResourceName, "identity.0.tenant_id", ""), + ), + }, + data.ImportStep(), + }, + }) +} + func TestAccAzureRMFunctionApp_loggingDisabled(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_function_app", "test") @@ -1422,6 +1456,106 @@ resource "azurerm_function_app" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomString) } +func testAccAzureRMFunctionApp_userAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +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_user_assigned_identity" "first" { + name = "acctest1%[1]d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" +} + +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_connection_string = "${azurerm_storage_account.test.primary_connection_string}" + + identity { + type = "UserAssigned" + identity_ids = ["${azurerm_user_assigned_identity.first.id}"] + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} + +func testAccAzureRMFunctionApp_userAssignedIdentityUpdated(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +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_user_assigned_identity" "first" { + name = "acctest1%[1]d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" +} + +resource "azurerm_user_assigned_identity" "second" { + name = "acctest2%[1]d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "${azurerm_resource_group.test.location}" +} + +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_connection_string = "${azurerm_storage_account.test.primary_connection_string}" + + identity { + type = "UserAssigned" + identity_ids = ["${azurerm_user_assigned_identity.first.id}", "${azurerm_user_assigned_identity.second.id}"] + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} + func testAccAzureRMFunctionApp_loggingDisabled(data acceptance.TestData) string { return fmt.Sprintf(` resource "azurerm_resource_group" "test" { diff --git a/website/docs/r/function_app.html.markdown b/website/docs/r/function_app.html.markdown index 9732e1e75235..71d78acdc7d1 100644 --- a/website/docs/r/function_app.html.markdown +++ b/website/docs/r/function_app.html.markdown @@ -164,9 +164,13 @@ A `cors` block supports the following: --- -`identity` supports the following: +An `identity` block supports the following: -* `type` - (Required) Specifies the identity type of the App Service. At this time the only allowed value is `SystemAssigned`. +* `type` - (Required) Specifies the identity type of the Function App. Possible values are `SystemAssigned` (where Azure will generate a Service Principal for you), `UserAssigned` where you can specify the Service Principal IDs in the `identity_ids` field, and `SystemAssigned, UserAssigned` which assigns both a system managed identity as well as the specified user assigned identities. + +~> **NOTE:** When `type` is set to `SystemAssigned`, The assigned `principal_id` and `tenant_id` can be retrieved after the Function App has been created. More details are available below. + +* `identity_ids` - (Optional) Specifies a list of user managed identity ids to be assigned. Required if `type` is `UserAssigned`. ---