diff --git a/azurerm/resource_arm_app_service.go b/azurerm/resource_arm_app_service.go index 9974f20172e5..a79646a8b10a 100644 --- a/azurerm/resource_arm_app_service.go +++ b/azurerm/resource_arm_app_service.go @@ -29,6 +29,33 @@ func resourceArmAppService() *schema.Resource { ValidateFunc: validateAppServiceName, }, + "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: ignoreCaseDiffSuppressFunc, + ValidateFunc: validation.StringInSlice([]string{ + "SystemAssigned", + }, true), + }, + "principal_id": { + Type: schema.TypeString, + Computed: true, + }, + "tenant_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "resource_group_name": resourceGroupNameSchema(), "location": locationSchema(), @@ -326,6 +353,11 @@ func resourceArmAppServiceCreate(d *schema.ResourceData, meta interface{}) error }, } + if _, ok := d.GetOk("identity"); ok { + appServiceIdentity := expandAzureRmAppServiceIdentity(d) + siteEnvelope.Identity = appServiceIdentity + } + if v, ok := d.GetOkExists("client_affinity_enabled"); ok { enabled := v.(bool) siteEnvelope.SiteProperties.ClientAffinityEnabled = utils.Bool(enabled) @@ -449,6 +481,28 @@ func resourceArmAppServiceUpdate(d *schema.ResourceData, meta interface{}) error } } + if d.HasChange("identity") { + site, err := client.Get(ctx, resGroup, name) + if err != nil { + return fmt.Errorf("Error getting configuration for App Service %q: %+v", name, err) + } + + appServiceIdentity := expandAzureRmAppServiceIdentity(d) + site.Identity = appServiceIdentity + + future, err := client.CreateOrUpdate(ctx, resGroup, name, site) + + if err != nil { + return fmt.Errorf("Error updating Managed Service Identity for App Service %q: %+v", name, err) + } + + err = future.WaitForCompletion(ctx, client.Client) + + if err != nil { + return fmt.Errorf("Error updating Managed Service Identity for App Service %q: %+v", name, err) + } + } + return resourceArmAppServiceRead(d, meta) } @@ -546,6 +600,11 @@ func resourceArmAppServiceRead(d *schema.ResourceData, meta interface{}) error { flattenAndSetTags(d, resp.Tags) + identity := flattenAzureRmAppServiceMachineIdentity(resp.Identity) + if err := d.Set("identity", identity); err != nil { + return err + } + return nil } @@ -802,6 +861,33 @@ func flattenAppServiceAppSettings(input map[string]*string) map[string]string { return output } +func expandAzureRmAppServiceIdentity(d *schema.ResourceData) *web.ManagedServiceIdentity { + identities := d.Get("identity").([]interface{}) + identity := identities[0].(map[string]interface{}) + identityType := identity["type"].(string) + return &web.ManagedServiceIdentity{ + Type: web.ManagedServiceIdentityType(identityType), + } +} + +func flattenAzureRmAppServiceMachineIdentity(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 validateAppServiceName(v interface{}, k string) (ws []string, es []error) { value := v.(string) diff --git a/azurerm/resource_arm_app_service_test.go b/azurerm/resource_arm_app_service_test.go index 8c5fce4d1169..51b180d7d5b6 100644 --- a/azurerm/resource_arm_app_service_test.go +++ b/azurerm/resource_arm_app_service_test.go @@ -2,6 +2,7 @@ package azurerm import ( "fmt" + "regexp" "testing" "github.com/hashicorp/terraform/helper/acctest" @@ -236,6 +237,67 @@ func TestAccAzureRMAppService_clientAffinityDisabled(t *testing.T) { }) } +func TestAccAzureRMAppService_enableManageServiceIdentity(t *testing.T) { + + resourceName := "azurerm_app_service.test" + ri := acctest.RandInt() + config := testAccAzureRMAppService_mangedServiceIdentity(ri, testLocation()) + + uuidMatch := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMAppServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "identity.0.type", "SystemAssigned"), + resource.TestMatchResourceAttr(resourceName, "identity.0.principal_id", uuidMatch), + resource.TestMatchResourceAttr(resourceName, "identity.0.tenant_id", uuidMatch), + ), + }, + }, + }) +} + +func TestAccAzureRMAppService_updateResourceByEnablingManageServiceIdentity(t *testing.T) { + + resourceName := "azurerm_app_service.test" + ri := acctest.RandInt() + + basicResourceNoManagedIdentity := testAccAzureRMAppService_basic(ri, testLocation()) + managedIdentityEnabled := testAccAzureRMAppService_mangedServiceIdentity(ri, testLocation()) + + uuidMatch := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, + Steps: []resource.TestStep{ + { + Config: basicResourceNoManagedIdentity, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMAppServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "identity.#", "0"), + ), + }, + { + Config: managedIdentityEnabled, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMAppServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "identity.0.type", "SystemAssigned"), + resource.TestMatchResourceAttr(resourceName, "identity.0.principal_id", uuidMatch), + resource.TestMatchResourceAttr(resourceName, "identity.0.tenant_id", uuidMatch), + ), + }, + }, + }) +} + func TestAccAzureRMAppService_clientAffinityUpdate(t *testing.T) { resourceName := "azurerm_app_service.test" ri := acctest.RandInt() @@ -976,6 +1038,36 @@ resource "azurerm_app_service" "test" { `, rInt, location, rInt, rInt, clientAffinity) } +func testAccAzureRMAppService_mangedServiceIdentity(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_app_service_plan" "test" { + name = "acctestASP-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + + sku { + tier = "Standard" + size = "S1" + } +} + +resource "azurerm_app_service" "test" { + name = "acctestAS-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + app_service_plan_id = "${azurerm_app_service_plan.test.id}" + identity = { + type = "SystemAssigned" + } +} +`, rInt, location, rInt, rInt) +} + func testAccAzureRMAppService_connectionStrings(rInt int, location string) string { return fmt.Sprintf(` resource "azurerm_resource_group" "test" { diff --git a/website/docs/r/app_service.html.markdown b/website/docs/r/app_service.html.markdown index 85b50930675d..4767393bd696 100644 --- a/website/docs/r/app_service.html.markdown +++ b/website/docs/r/app_service.html.markdown @@ -131,6 +131,8 @@ The following arguments are supported: * `tags` - (Optional) A mapping of tags to assign to the resource. +* `identity` - (Optional) A Managed Service Identity block as defined below. + --- `connection_string` supports the following: @@ -141,6 +143,14 @@ The following arguments are supported: --- +`identity` supports the following: + +* `type` - (Required) Specifies the identity type of the App Service. At this time the only allowed value is `SystemAssigned`. + +~> The assigned `principal_id` and `tenant_id` can be retrieved after the App Service has been created. More details are available below. + +--- + `site_config` supports the following: * `always_on` - (Optional) Should the app be loaded at all times? Defaults to `false`. @@ -167,7 +177,6 @@ The following arguments are supported: * `websockets_enabled` - (Optional) Should WebSockets be enabled? - ~> **NOTE:** Additional Source Control types will be added in the future, once support for them has been added in the Azure SDK for Go. ## Attributes Reference @@ -180,21 +189,35 @@ The following attributes are exported: * `outbound_ip_addresses` - A comma separated list of outbound IP addresses - such as `52.23.25.3,52.143.43.12` -* `source_control` - (Optional) The default local Git source control information if deployment option is set to `LocalGit`. +* `source_control` - A `source_control` block as defined below, which contains the Source Control information when `scm_type` is set to `LocalGit`. + +* `site_credential` - A `site_credential` block as defined below, which contains the site-level credentials used to publish to this App Service. -* `site_credential` - (Optional) The site-level credential used to publish files to Azure Web App. +* `identity` - An `identity` block as defined below, which contains the Managed Service Identity information for this App Service. --- -`source_control` supports the following: +`identity` exports the following: -* `repo_url` - URL of the Git repository for this App Service. -* `branch` - Branch name of the Git repository for this App Service. +* `principal_id` - The Principal ID for the Service Principal associated with the Managed Service Identity of this App Service. -`site_credential` supports the following: +* `tenant_id` - The Tenant ID for the Service Principal associated with the Managed Service Identity of this App Service. -* `username` - If your site is named 'MySite', the user name will be '$MySite'. -* `password` - Some long random string. +--- + +`site_credential` exports the following: + +* `username` - The username which can be used to publish to this App Service +* `password` - The password associated with the username, which can be used to publish to this App Service. + +~> **NOTE:** both `username` and `password` for the `site_credential` block are only exported when `scm_type` is set to `LocalGit` + +--- + +`source_control` exports the following: + +* `repo_url` - URL of the Git repository for this App Service. +* `branch` - Branch name of the Git repository for this App Service. ## Import