Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

azurerm_app_service - support for MSI #1130

Merged
merged 8 commits into from
May 1, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions azurerm/resource_arm_app_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log"
"regexp"

"context"
"github.com/Azure/azure-sdk-for-go/services/web/mgmt/2016-09-01/web"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
Expand All @@ -29,6 +30,33 @@ func resourceArmAppService() *schema.Resource {
ValidateFunc: validateAppServiceName,
},

"identity": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we document these properties in the documentation? That's stored in this file, it'll want the type property documented in the Argument Reference block and the principal_id and tenant_id fields documented in the Attributes Reference block

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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(),
Expand Down Expand Up @@ -336,6 +364,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)
Expand Down Expand Up @@ -364,6 +397,16 @@ func resourceArmAppServiceCreate(d *schema.ResourceData, meta interface{}) error
return resourceArmAppServiceUpdate(d, meta)
}

func expandAzureRmAppServiceIdentity(d *schema.ResourceData) *web.ManagedServiceIdentity {
v := d.Get("identity")
identities := v.([]interface{})
identity := identities[0].(map[string]interface{})
identityType := identity["type"].(string)
return &web.ManagedServiceIdentity{
Type: web.ManagedServiceIdentityType(identityType),
}
}

func resourceArmAppServiceUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient).appServicesClient
ctx := meta.(*ArmClient).StopContext
Expand Down Expand Up @@ -431,9 +474,47 @@ 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

err = updateAppServiceSettings(client, ctx, resGroup, name, site)
if err != nil {
return err
}
}

return resourceArmAppServiceRead(d, meta)
}

func updateAppServiceSettings(
client web.AppsClient,
ctx context.Context,
resGroup string,
name string,
site web.Site) error {

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 nil
}

func resourceArmAppServiceRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient).appServicesClient

Expand Down Expand Up @@ -528,9 +609,33 @@ 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
}

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 resourceArmAppServiceDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient).appServicesClient

Expand Down
92 changes: 92 additions & 0 deletions azurerm/resource_arm_app_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package azurerm

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform/helper/acctest"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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" {
Expand Down
42 changes: 42 additions & 0 deletions website/docs/r/app_service.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ The following arguments are supported:

* `tags` - (Optional) A mapping of tags to assign to the resource. Changing this forces a new resource to be created.

* `identity` - (Optional) A Managed Service Identity block as defined below.

---

`connection_string` supports the following:
Expand All @@ -153,6 +155,43 @@ The following arguments are supported:

* `local_mysql_enabled` - (Optional) Is "MySQL In App" Enabled? This runs a local MySQL instance with your app and shares resources from the App Service plan.

---

`identity` supports the following:

* `type` - (Required) Specifies the identity type of the App Service. The only allowable value is `SystemAssigned`.
The assigned `principal_id` and `tenant_id` can be retrieved after the App Service has been created, e.g.

```hcl
resource "azurerm_app_service" "test" {
name = "${random_id.server.hex}"
location = "${azurerm_resource_group.test.location}"
resource_group_name = "${azurerm_resource_group.test.name}"
app_service_plan_id = "${azurerm_app_service_plan.test.id}"

site_config {
always_on = true
}

identity = {
type = "SystemAssigned"
}

client_affinity_enabled = false

app_settings {
"SOME_KEY" = "some-value"
}

}

output "test_service_principal_id" {
value = "${lookup(azurerm_app_service.test.identity[0], "principal_id")}"
}
output "test_service_tenant_id" {
value = "${lookup(azurerm_app_service.test.identity[0], "tenant_id")}"
}
```
~> **NOTE:** MySQL In App is not intended for production environments and will not scale beyond a single instance. Instead you may wish [to use Azure Database for MySQL](/docs/providers/azurerm/r/mysql_database.html).

* `managed_pipeline_mode` - (Optional) The Managed Pipeline Mode. Possible values are `Integrated` and `Classic`. Defaults to `Integrated`.
Expand Down Expand Up @@ -184,6 +223,9 @@ The following attributes are exported:

* `site_credential` - (Optional) The site-level credential used to publish files to Azure Web App.

* `principal_id` - When Identity is `SystemAssigned` attribute exposes Service Principal id created by Azure Resource Manager.

* `tenant_id` - When Identity is `SystemAssigned` attribute exposes Azure AD tenant id where Service Principal was created.
---

`source_control` supports the following:
Expand Down