Skip to content

Commit

Permalink
Merge pull request #5676 from terraform-providers/f/function-app-uai
Browse files Browse the repository at this point in the history
r/function_app: support for User Assigned Identities
  • Loading branch information
tombuildsstuff authored Feb 11, 2020
2 parents 86cba2f + 6bc649a commit 7f2a742
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 67 deletions.
20 changes: 13 additions & 7 deletions azurerm/helpers/azure/app_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
74 changes: 16 additions & 58 deletions azurerm/internal/services/web/resource_arm_function_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,32 +134,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(),

Expand Down Expand Up @@ -364,10 +339,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)
Expand Down Expand Up @@ -451,19 +426,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)
Expand Down Expand Up @@ -591,10 +566,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"])
Expand All @@ -616,6 +587,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)
Expand Down Expand Up @@ -907,24 +883,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{})
Expand Down
134 changes: 134 additions & 0 deletions azurerm/internal/services/web/tests/resource_arm_function_app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -1520,6 +1554,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" {
Expand Down
8 changes: 6 additions & 2 deletions website/docs/r/function_app.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,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`.

---

Expand Down

0 comments on commit 7f2a742

Please sign in to comment.