diff --git a/azurerm/internal/services/apimanagement/api_management_data_source.go b/azurerm/internal/services/apimanagement/api_management_data_source.go index a0e2b3551213..66ae015482c0 100644 --- a/azurerm/internal/services/apimanagement/api_management_data_source.go +++ b/azurerm/internal/services/apimanagement/api_management_data_source.go @@ -52,6 +52,34 @@ func dataSourceApiManagementService() *schema.Resource { Computed: true, }, + "identity": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Computed: true, + }, + "principal_id": { + Type: schema.TypeString, + Computed: true, + }, + "tenant_id": { + Type: schema.TypeString, + Computed: true, + }, + "identity_ids": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "notification_sender_email": { Type: schema.TypeString, Computed: true, diff --git a/azurerm/internal/services/apimanagement/api_management_resource.go b/azurerm/internal/services/apimanagement/api_management_resource.go index a2070eeda627..d75305d26b80 100644 --- a/azurerm/internal/services/apimanagement/api_management_resource.go +++ b/azurerm/internal/services/apimanagement/api_management_resource.go @@ -87,9 +87,13 @@ func resourceArmApiManagementService() *schema.Resource { Schema: map[string]*schema.Schema{ "type": { Type: schema.TypeString, - Required: true, + Optional: true, + Default: string(apimanagement.None), ValidateFunc: validation.StringInSlice([]string{ - "SystemAssigned", + string(apimanagement.None), + string(apimanagement.SystemAssigned), + string(apimanagement.UserAssigned), + string(apimanagement.SystemAssignedUserAssigned), }, false), }, "principal_id": { @@ -100,6 +104,15 @@ func resourceArmApiManagementService() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "identity_ids": { + Type: schema.TypeSet, + Optional: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.NoZeroValues, + }, + }, }, }, }, @@ -477,7 +490,11 @@ func resourceArmApiManagementServiceCreateUpdate(d *schema.ResourceData, meta in } if _, ok := d.GetOk("identity"); ok { - properties.Identity = expandAzureRmApiManagementIdentity(d) + identity, err := expandAzureRmApiManagementIdentity(d) + if err != nil { + return fmt.Errorf("Error expanding `identity`: %+v", err) + } + properties.Identity = identity } if _, ok := d.GetOk("additional_location"); ok { @@ -929,17 +946,43 @@ func flattenApiManagementAdditionalLocations(input *[]apimanagement.AdditionalLo return results } -func expandAzureRmApiManagementIdentity(d *schema.ResourceData) *apimanagement.ServiceIdentity { +func expandAzureRmApiManagementIdentity(d *schema.ResourceData) (*apimanagement.ServiceIdentity, error) { + var identityIdSet *schema.Set + managedServiceIdentity := apimanagement.ServiceIdentity{} + vs := d.Get("identity").([]interface{}) if len(vs) == 0 { - return nil + managedServiceIdentity.Type = apimanagement.None + } else { + v := vs[0].(map[string]interface{}) + identityType, exists := v["type"] + if !exists { + return nil, fmt.Errorf("`type` must be specified when `identity` is set") + } + managedServiceIdentity.Type = apimanagement.ApimIdentityType(identityType.(string)) + if identityIds, exists := v["identity_ids"]; exists { + identityIdSet = (identityIds.(*schema.Set)) + } } - v := vs[0].(map[string]interface{}) - identityType := v["type"].(string) - return &apimanagement.ServiceIdentity{ - Type: apimanagement.ApimIdentityType(identityType), + // If type contains `UserAssigned`, `identity_ids` must be specified and have at least 1 element + if managedServiceIdentity.Type == apimanagement.UserAssigned || managedServiceIdentity.Type == apimanagement.SystemAssignedUserAssigned { + if identityIdSet == nil || identityIdSet.Len() == 0 { + return nil, fmt.Errorf("`identity_ids` must have at least 1 element when `type` includes `UserAssigned`") + } + + userAssignedIdentities := make(map[string]*apimanagement.UserIdentityProperties) + for _, id := range identityIdSet.List() { + userAssignedIdentities[id.(string)] = &apimanagement.UserIdentityProperties{} + } + + managedServiceIdentity.UserAssignedIdentities = userAssignedIdentities + } else if identityIdSet != nil && identityIdSet.Len() > 0 { + // If type does _not_ contain `UserAssigned` (i.e. is set to `SystemAssigned` or defaulted to `None`), `identity_ids` is not allowed + return nil, fmt.Errorf("`identity_ids` can only be specified when `type` includes `UserAssigned`; but `type` is currently %q", managedServiceIdentity.Type) } + + return &managedServiceIdentity, nil } func flattenAzureRmApiManagementMachineIdentity(identity *apimanagement.ServiceIdentity) []interface{} { @@ -959,6 +1002,14 @@ func flattenAzureRmApiManagementMachineIdentity(identity *apimanagement.ServiceI result["tenant_id"] = identity.TenantID.String() } + identityIds := make([]interface{}, 0) + if identity.UserAssignedIdentities != nil { + for key := range identity.UserAssignedIdentities { + identityIds = append(identityIds, key) + } + result["identity_ids"] = schema.NewSet(schema.HashString, identityIds) + } + return []interface{}{result} } diff --git a/azurerm/internal/services/apimanagement/tests/api_management_resource_test.go b/azurerm/internal/services/apimanagement/tests/api_management_resource_test.go index 8418648f806d..959578090643 100644 --- a/azurerm/internal/services/apimanagement/tests/api_management_resource_test.go +++ b/azurerm/internal/services/apimanagement/tests/api_management_resource_test.go @@ -84,6 +84,8 @@ func TestAccAzureRMApiManagement_complete(t *testing.T) { resource.TestCheckResourceAttr(data.ResourceName, "tags.Acceptance", "Test"), resource.TestCheckResourceAttrSet(data.ResourceName, "public_ip_addresses.#"), resource.TestCheckResourceAttr(data.ResourceName, "protocols.0.enable_http2", "true"), + resource.TestCheckResourceAttr(data.ResourceName, "identity.#", "1"), + resource.TestCheckResourceAttr(data.ResourceName, "identity.0.type", "None"), resource.TestCheckResourceAttr(data.ResourceName, "hostname_configuration.0.proxy.0.host_name", "api.terraform.io"), resource.TestCheckResourceAttr(data.ResourceName, "hostname_configuration.0.proxy.1.host_name", "api2.terraform.io"), resource.TestCheckResourceAttr(data.ResourceName, "hostname_configuration.0.portal.0.host_name", "portal.terraform.io"), @@ -246,6 +248,271 @@ func testCheckAzureRMApiManagementExists(resourceName string) resource.TestCheck } } +func TestAccAzureRMApiManagement_identityUserAssigned(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_identityUserAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMApiManagement_identityNoneUpdateUserAssigned(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_identityNone(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMApiManagement_identityUserAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMApiManagement_identityUserAssignedUpdateNone(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_identityUserAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMApiManagement_identityNone(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMApiManagement_identitySystemAssigned(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_identitySystemAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMApiManagement_identitySystemAssignedUpdateNone(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_identitySystemAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMApiManagement_identityNone(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMApiManagement_identityNoneUpdateSystemAssigned(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_identityNone(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMApiManagement_identitySystemAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMApiManagement_identitySystemAssignedUserAssigned(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_identitySystemAssignedUserAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMApiManagement_identitySystemAssignedUserAssignedUpdateNone(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_identitySystemAssignedUserAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMApiManagement_identityNone(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMApiManagement_identityNoneUpdateSystemAssignedUserAssigned(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_identityNone(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMApiManagement_identitySystemAssignedUserAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMApiManagement_identitySystemAssignedUserAssignedUpdateSystemAssigned(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_identitySystemAssignedUserAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMApiManagement_identitySystemAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMApiManagement_identitySystemAssignedUserAssignedUpdateUserAssigned(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagement_identitySystemAssignedUserAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMApiManagement_identityUserAssigned(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + func testAccAzureRMApiManagement_basic(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { @@ -580,3 +847,129 @@ resource "azurerm_api_management" "test" { } `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomInteger) } + +func testAccAzureRMApiManagement_identityUserAssigned(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_user_assigned_identity" "test" { + name = "acctestUAI-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_api_management" "test" { + name = "acctestAM-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + publisher_name = "pub1" + publisher_email = "pub1@email.com" + + sku_name = "Developer_1" + + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) +} + +func testAccAzureRMApiManagement_identitySystemAssigned(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_api_management" "test" { + name = "acctestAM-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + publisher_name = "pub1" + publisher_email = "pub1@email.com" + + sku_name = "Developer_1" + + identity { + type = "SystemAssigned" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} + +func testAccAzureRMApiManagement_identitySystemAssignedUserAssigned(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_user_assigned_identity" "test" { + name = "acctestUAI-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_api_management" "test" { + name = "acctestAM-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + publisher_name = "pub1" + publisher_email = "pub1@email.com" + + sku_name = "Developer_1" + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) +} + +func testAccAzureRMApiManagement_identityNone(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_api_management" "test" { + name = "acctestAM-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + publisher_name = "pub1" + publisher_email = "pub1@email.com" + + sku_name = "Developer_1" + + identity { + type = "None" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} diff --git a/website/docs/d/api_management.html.markdown b/website/docs/d/api_management.html.markdown index 2c6876288410..349a3ee98877 100644 --- a/website/docs/d/api_management.html.markdown +++ b/website/docs/d/api_management.html.markdown @@ -41,6 +41,8 @@ output "api_management_id" { * `gateway_regional_url` - The URL for the Gateway in the Default Region. +* `identity` - (Optional) An `identity` block as defined below. + * `hostname_configuration` - A `hostname_configuration` block as defined below. * `management_api_url` - The URL for the Management API. @@ -73,6 +75,20 @@ A `additional_location` block exports the following: --- +A `identity` block exports the following: + +~> **Note:** User Assigned Managed Identities are in Preview + +* `type` - Specifies the type of Managed Service Identity that is configured on this API Management Service. + +* `principal_id` - Specifies the Principal ID of the System Assigned Managed Service Identity that is configured on this API Management Service. + +* `tenant_id` - Specifies the Tenant ID of the System Assigned Managed Service Identity that is configured on this API Management Service. + +* `identity_ids` - A list of IDs for User Assigned Managed Identity resources to be assigned. + +--- + A `hostname_configuration` block exports the following: * `management` - One or more `management` blocks as documented below. diff --git a/website/docs/r/api_management.html.markdown b/website/docs/r/api_management.html.markdown index 2304a0b4781b..2da6eb4a6695 100644 --- a/website/docs/r/api_management.html.markdown +++ b/website/docs/r/api_management.html.markdown @@ -120,7 +120,13 @@ A `hostname_configuration` block supports the following: A `identity` block supports the following: -* `type` - (Required) Specifies the type of Managed Service Identity that should be configured on this API Management Service. At this time the only supported value is`SystemAssigned`. +~> **Note:** User Assigned Managed Identities are in Preview + +* `type` - (Required) Specifies the type of Managed Service Identity that should be configured on this API Management Service. Possible values are `SystemAssigned`, `UserAssigned` or `SystemAssigned, UserAssigned` (to enable both). + +* `identity_ids` - (Optional) A list of IDs for User Assigned Managed Identity resources to be assigned. + +~> **NOTE:** This is required when `type` is set to `UserAssigned` or `SystemAssigned, UserAssigned`. ---