diff --git a/azurerm/helpers/azure/api_management.go b/azurerm/helpers/azure/api_management.go index ce28eb212c40..61bacd508834 100644 --- a/azurerm/helpers/azure/api_management.go +++ b/azurerm/helpers/azure/api_management.go @@ -309,3 +309,20 @@ func FlattenApiManagementOperationParameterContract(input *[]apimanagement.Param return outputs } + +// CopyCertificateAndPassword copies any certificate and password attributes +// from the old config to the current to avoid state diffs. +// Iterate through old state to find sensitive props not returned by API. +// This must be done in order to avoid state diffs. +// NOTE: this information won't be available during times like Import, so this is a best-effort. +func CopyCertificateAndPassword(vals []interface{}, hostName string, output map[string]interface{}) { + for _, val := range vals { + oldConfig := val.(map[string]interface{}) + + if oldConfig["host_name"] == hostName { + output["certificate_password"] = oldConfig["certificate_password"] + output["certificate"] = oldConfig["certificate"] + break + } + } +} diff --git a/azurerm/internal/services/apimanagement/api_management_custom_domain_resource.go b/azurerm/internal/services/apimanagement/api_management_custom_domain_resource.go new file mode 100644 index 000000000000..12215fe9d39b --- /dev/null +++ b/azurerm/internal/services/apimanagement/api_management_custom_domain_resource.go @@ -0,0 +1,377 @@ +package apimanagement + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/services/apimanagement/mgmt/2019-12-01/apimanagement" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/apimanagement/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +var apiManagementCustomDomainResourceName = "azurerm_api_management_custom_domain" + +func resourceArmApiManagementCustomDomain() *schema.Resource { + return &schema.Resource{ + Create: apiManagementCustomDomainCreateUpdate, + Read: apiManagementCustomDomainRead, + Update: apiManagementCustomDomainCreateUpdate, + Delete: apiManagementCustomDomainDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "api_management_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: azure.ValidateResourceID, + }, + + "management": { + Type: schema.TypeList, + Optional: true, + AtLeastOneOf: []string{"management", "portal", "developer_portal", "proxy", "scm"}, + Elem: &schema.Resource{ + Schema: apiManagementResourceHostnameSchema(), + }, + }, + "portal": { + Type: schema.TypeList, + Optional: true, + AtLeastOneOf: []string{"management", "portal", "developer_portal", "proxy", "scm"}, + Elem: &schema.Resource{ + Schema: apiManagementResourceHostnameSchema(), + }, + }, + "developer_portal": { + Type: schema.TypeList, + Optional: true, + AtLeastOneOf: []string{"management", "portal", "developer_portal", "proxy", "scm"}, + Elem: &schema.Resource{ + Schema: apiManagementResourceHostnameSchema(), + }, + }, + "proxy": { + Type: schema.TypeList, + Optional: true, + AtLeastOneOf: []string{"management", "portal", "developer_portal", "proxy", "scm"}, + Elem: &schema.Resource{ + Schema: apiManagementResourceHostnameProxySchema(), + }, + }, + "scm": { + Type: schema.TypeList, + Optional: true, + AtLeastOneOf: []string{"management", "portal", "developer_portal", "proxy", "scm"}, + Elem: &schema.Resource{ + Schema: apiManagementResourceHostnameSchema(), + }, + }, + }, + } +} + +func apiManagementCustomDomainCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).ApiManagement.ServiceClient + ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) + defer cancel() + + log.Printf("[INFO] preparing arguments for API Management Custom domain creation.") + + apiManagementID := d.Get("api_management_id").(string) + id, err := parse.ApiManagementID(apiManagementID) + if err != nil { + return err + } + resourceGroup := id.ResourceGroup + serviceName := id.ServiceName + + existing, err := client.Get(ctx, resourceGroup, serviceName) + if err != nil { + return fmt.Errorf("finding API Management (API Management %q / Resource Group %q): %s", serviceName, resourceGroup, err) + } + + if d.IsNewResource() { + if existing.ServiceProperties != nil && existing.ServiceProperties.HostnameConfigurations != nil && len(*existing.ServiceProperties.HostnameConfigurations) > 1 { + return tf.ImportAsExistsError(apiManagementCustomDomainResourceName, *existing.ID) + } + } + + existing.ServiceProperties.HostnameConfigurations = expandApiManagementCustomDomains(d) + + // Wait for the ProvisioningState to become "Succeeded" before attempting to update + log.Printf("[DEBUG] Waiting for API Management Service %q (Resource Group: %q) to become ready", serviceName, resourceGroup) + stateConf := &resource.StateChangeConf{ + Pending: []string{"Updating", "Unknown"}, + Target: []string{"Succeeded", "Ready"}, + Refresh: apiManagementRefreshFunc(ctx, client, serviceName, resourceGroup), + MinTimeout: 1 * time.Minute, + ContinuousTargetOccurence: 6, + } + if d.IsNewResource() { + stateConf.Timeout = d.Timeout(schema.TimeoutCreate) + } else { + stateConf.Timeout = d.Timeout(schema.TimeoutUpdate) + } + + if _, err = stateConf.WaitForState(); err != nil { + return fmt.Errorf("waiting for API Management Service %q (Resource Group: %q) to become ready: %+v", serviceName, resourceGroup, err) + } + + if _, err := client.CreateOrUpdate(ctx, resourceGroup, serviceName, existing); err != nil { + return fmt.Errorf("creating/updating Custom Domain (API Management %q / Resource Group %q): %+v", serviceName, resourceGroup, err) + } + + read, err := client.Get(ctx, resourceGroup, serviceName) + if err != nil { + return fmt.Errorf("retrieving Custom Domain (API Management %q / Resource Group %q): %+v", serviceName, resourceGroup, err) + } + if read.ID == nil { + return fmt.Errorf("cannot read ID for Custom Domain (API Management %q / Resource Group %q)", serviceName, resourceGroup) + } + + // Wait for the ProvisioningState to become "Succeeded" before attempting to update + log.Printf("[DEBUG] Waiting for API Management Service %q (Resource Group: %q) to become ready", serviceName, resourceGroup) + if _, err = stateConf.WaitForState(); err != nil { + return fmt.Errorf("waiting for API Management Service %q (Resource Group: %q) to become ready: %+v", serviceName, resourceGroup, err) + } + + customDomainsID := fmt.Sprintf("%s/customDomains/default", *read.ID) + d.SetId(customDomainsID) + + return apiManagementCustomDomainRead(d, meta) +} + +func apiManagementCustomDomainRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).ApiManagement.ServiceClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.ApiManagementCustomDomainID(d.Id()) + if err != nil { + return err + } + resourceGroup := id.ResourceGroup + serviceName := id.ServiceName + + resp, err := client.Get(ctx, resourceGroup, serviceName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("API Management Service %q was not found in Resource Group %q - removing from state!", serviceName, resourceGroup) + d.SetId("") + return nil + } + + return fmt.Errorf("making Read request on API Management Service %q (Resource Group %q): %+v", serviceName, resourceGroup, err) + } + + d.Set("api_management_id", resp.ID) + + if resp.ServiceProperties != nil && resp.ServiceProperties.HostnameConfigurations != nil { + configs := flattenApiManagementHostnameConfiguration(resp.ServiceProperties.HostnameConfigurations, d) + for _, config := range configs { + for key, v := range config.(map[string]interface{}) { + if err := d.Set(key, v); err != nil { + return fmt.Errorf("setting `hostname_configuration` %q: %+v", key, err) + } + } + } + } + + return nil +} + +func apiManagementCustomDomainDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).ApiManagement.ServiceClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.ApiManagementCustomDomainID(d.Id()) + if err != nil { + return err + } + resourceGroup := id.ResourceGroup + serviceName := id.ServiceName + + resp, err := client.Get(ctx, resourceGroup, serviceName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("API Management Service %q was not found in Resource Group %q - removing from state!", serviceName, resourceGroup) + d.SetId("") + return nil + } + + return fmt.Errorf("making Read request on API Management Service %q (Resource Group %q): %+v", serviceName, resourceGroup, err) + } + + // Wait for the ProvisioningState to become "Succeeded" before attempting to update + log.Printf("[DEBUG] Waiting for API Management Service %q (Resource Group: %q) to become ready", serviceName, resourceGroup) + stateConf := &resource.StateChangeConf{ + Pending: []string{"Updating", "Unknown"}, + Target: []string{"Succeeded", "Ready"}, + Refresh: apiManagementRefreshFunc(ctx, client, serviceName, resourceGroup), + MinTimeout: 1 * time.Minute, + Timeout: d.Timeout(schema.TimeoutDelete), + ContinuousTargetOccurence: 6, + } + + if _, err = stateConf.WaitForState(); err != nil { + return fmt.Errorf("waiting for API Management Service %q (Resource Group: %q) to become ready: %+v", serviceName, resourceGroup, err) + } + + log.Printf("[DEBUG] Deleting API Management Custom Domain (API Management %q / Resource Group %q)", serviceName, resourceGroup) + + resp.ServiceProperties.HostnameConfigurations = nil + + if _, err := client.CreateOrUpdate(ctx, resourceGroup, serviceName, resp); err != nil { + return fmt.Errorf("deleting Custom Domain (API Management %q / Resource Group %q): %+v", serviceName, resourceGroup, err) + } + + // Wait for the ProvisioningState to become "Succeeded" before attempting to update + log.Printf("[DEBUG] Waiting for API Management Service %q (Resource Group: %q) to become ready", serviceName, resourceGroup) + if _, err = stateConf.WaitForState(); err != nil { + return fmt.Errorf("waiting for API Management Service %q (Resource Group: %q) to become ready: %+v", serviceName, resourceGroup, err) + } + + return nil +} + +func expandApiManagementCustomDomains(input *schema.ResourceData) *[]apimanagement.HostnameConfiguration { + results := make([]apimanagement.HostnameConfiguration, 0) + + if managementRawVal, ok := input.GetOk("management"); ok { + vs := managementRawVal.([]interface{}) + for _, rawVal := range vs { + v := rawVal.(map[string]interface{}) + output := expandApiManagementCommonHostnameConfiguration(v, apimanagement.HostnameTypeManagement) + results = append(results, output) + } + } + if portalRawVal, ok := input.GetOk("portal"); ok { + vs := portalRawVal.([]interface{}) + for _, rawVal := range vs { + v := rawVal.(map[string]interface{}) + output := expandApiManagementCommonHostnameConfiguration(v, apimanagement.HostnameTypePortal) + results = append(results, output) + } + } + if developerPortalRawVal, ok := input.GetOk("developer_portal"); ok { + vs := developerPortalRawVal.([]interface{}) + for _, rawVal := range vs { + v := rawVal.(map[string]interface{}) + output := expandApiManagementCommonHostnameConfiguration(v, apimanagement.HostnameTypeDeveloperPortal) + results = append(results, output) + } + } + if proxyRawVal, ok := input.GetOk("proxy"); ok { + vs := proxyRawVal.([]interface{}) + for _, rawVal := range vs { + v := rawVal.(map[string]interface{}) + output := expandApiManagementCommonHostnameConfiguration(v, apimanagement.HostnameTypeProxy) + if value, ok := v["default_ssl_binding"]; ok { + output.DefaultSslBinding = utils.Bool(value.(bool)) + } + results = append(results, output) + } + } + if scmRawVal, ok := input.GetOk("scm"); ok { + vs := scmRawVal.([]interface{}) + for _, rawVal := range vs { + v := rawVal.(map[string]interface{}) + output := expandApiManagementCommonHostnameConfiguration(v, apimanagement.HostnameTypeScm) + results = append(results, output) + } + } + return &results +} + +func flattenApiManagementHostnameConfiguration(input *[]apimanagement.HostnameConfiguration, d *schema.ResourceData) []interface{} { + results := make([]interface{}, 0) + if input == nil { + return results + } + + managementResults := make([]interface{}, 0) + portalResults := make([]interface{}, 0) + developerPortalResults := make([]interface{}, 0) + proxyResults := make([]interface{}, 0) + scmResults := make([]interface{}, 0) + + for _, config := range *input { + output := make(map[string]interface{}) + + if config.HostName != nil { + output["host_name"] = *config.HostName + } + + if config.NegotiateClientCertificate != nil { + output["negotiate_client_certificate"] = *config.NegotiateClientCertificate + } + + if config.KeyVaultID != nil { + output["key_vault_id"] = *config.KeyVaultID + } + + var configType string + switch strings.ToLower(string(config.Type)) { + case strings.ToLower(string(apimanagement.HostnameTypeProxy)): + // only set SSL binding for proxy types + if config.DefaultSslBinding != nil { + output["default_ssl_binding"] = *config.DefaultSslBinding + } + proxyResults = append(proxyResults, output) + configType = "proxy" + + case strings.ToLower(string(apimanagement.HostnameTypeManagement)): + managementResults = append(managementResults, output) + configType = "management" + + case strings.ToLower(string(apimanagement.HostnameTypePortal)): + portalResults = append(portalResults, output) + configType = "portal" + + case strings.ToLower(string(apimanagement.HostnameTypeDeveloperPortal)): + developerPortalResults = append(developerPortalResults, output) + configType = "developer_portal" + + case strings.ToLower(string(apimanagement.HostnameTypeScm)): + scmResults = append(scmResults, output) + configType = "scm" + } + + if configType != "" { + if valsRaw, ok := d.GetOk(configType); ok { + vals := valsRaw.([]interface{}) + azure.CopyCertificateAndPassword(vals, *config.HostName, output) + } + } + } + + return []interface{}{ + map[string]interface{}{ + "management": managementResults, + "portal": portalResults, + "developer_portal": developerPortalResults, + "proxy": proxyResults, + "scm": scmResults, + }, + } +} diff --git a/azurerm/internal/services/apimanagement/api_management_data_source.go b/azurerm/internal/services/apimanagement/api_management_data_source.go index 2bc43c2505b6..38af27e05295 100644 --- a/azurerm/internal/services/apimanagement/api_management_data_source.go +++ b/azurerm/internal/services/apimanagement/api_management_data_source.go @@ -229,6 +229,11 @@ func dataSourceApiManagementRead(d *schema.ResourceData, meta interface{}) error d.Set("location", azure.NormalizeLocation(*location)) } + identity := flattenAzureRmApiManagementMachineIdentity(resp.Identity) + if err := d.Set("identity", identity); err != nil { + return fmt.Errorf("setting `identity`: %+v", err) + } + if props := resp.ServiceProperties; props != nil { d.Set("publisher_email", props.PublisherEmail) d.Set("publisher_name", props.PublisherName) diff --git a/azurerm/internal/services/apimanagement/api_management_resource.go b/azurerm/internal/services/apimanagement/api_management_resource.go index 170c9b0b65f5..97643d4c93a9 100644 --- a/azurerm/internal/services/apimanagement/api_management_resource.go +++ b/azurerm/internal/services/apimanagement/api_management_resource.go @@ -1,6 +1,7 @@ package apimanagement import ( + "context" "fmt" "log" "strconv" @@ -9,12 +10,15 @@ import ( "github.com/Azure/azure-sdk-for-go/services/apimanagement/mgmt/2019-12-01/apimanagement" "github.com/hashicorp/go-azure-helpers/response" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/apimanagement/parse" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/suppress" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" @@ -503,21 +507,23 @@ func resourceArmApiManagementServiceCreateUpdate(d *schema.ResourceData, meta in customProperties := expandApiManagementCustomProperties(d) certificates := expandAzureRmApiManagementCertificates(d) - hostnameConfigurations := expandAzureRmApiManagementHostnameConfigurations(d) properties := apimanagement.ServiceResource{ Location: utils.String(location), ServiceProperties: &apimanagement.ServiceProperties{ - PublisherName: utils.String(publisherName), - PublisherEmail: utils.String(publisherEmail), - CustomProperties: customProperties, - Certificates: certificates, - HostnameConfigurations: hostnameConfigurations, + PublisherName: utils.String(publisherName), + PublisherEmail: utils.String(publisherEmail), + CustomProperties: customProperties, + Certificates: certificates, }, Tags: tags.Expand(t), Sku: sku, } + if _, ok := d.GetOk("hostname_configuration"); ok { + properties.ServiceProperties.HostnameConfigurations = expandAzureRmApiManagementHostnameConfigurations(d) + } + // intentionally not gated since we specify a default value (of None) in the expand, which we need on updates identityRaw := d.Get("identity").([]interface{}) identity, err := expandAzureRmApiManagementIdentity(identityRaw) @@ -615,13 +621,13 @@ func resourceArmApiManagementServiceRead(d *schema.ResourceData, meta interface{ ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) defer cancel() - id, err := azure.ParseAzureResourceID(d.Id()) + id, err := parse.ApiManagementID(d.Id()) if err != nil { return err } resourceGroup := id.ResourceGroup - name := id.Path["service"] + name := id.ServiceName resp, err := client.Get(ctx, resourceGroup, name) if err != nil { @@ -726,12 +732,12 @@ func resourceArmApiManagementServiceDelete(d *schema.ResourceData, meta interfac ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) defer cancel() - id, err := azure.ParseAzureResourceID(d.Id()) + id, err := parse.ApiManagementID(d.Id()) if err != nil { return err } resourceGroup := id.ResourceGroup - name := id.Path["service"] + name := id.ServiceName log.Printf("[DEBUG] Deleting API Management Service %q (Resource Grouo %q)", name, resourceGroup) future, err := client.Delete(ctx, resourceGroup, name) @@ -748,9 +754,38 @@ func resourceArmApiManagementServiceDelete(d *schema.ResourceData, meta interfac return nil } +func apiManagementRefreshFunc(ctx context.Context, client *apimanagement.ServiceClient, serviceName, resourceGroup string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + log.Printf("[DEBUG] Checking to see if API Management Service %q (Resource Group: %q) is available..", serviceName, resourceGroup) + + resp, err := client.Get(ctx, resourceGroup, serviceName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[DEBUG] Retrieving API Management %q (Resource Group: %q) returned 404.", serviceName, resourceGroup) + return nil, "NotFound", nil + } + + return nil, "", fmt.Errorf("Error polling for the state of the API Management Service %q (Resource Group: %q): %+v", serviceName, resourceGroup, err) + } + + state := "" + if props := resp.ServiceProperties; props != nil { + if props.ProvisioningState != nil { + state = *props.ProvisioningState + } + } + + return resp, state, nil + } +} + func expandAzureRmApiManagementHostnameConfigurations(d *schema.ResourceData) *[]apimanagement.HostnameConfiguration { results := make([]apimanagement.HostnameConfiguration, 0) - hostnameVs := d.Get("hostname_configuration").([]interface{}) + vs := d.Get("hostname_configuration") + if vs == nil { + return &results + } + hostnameVs := vs.([]interface{}) for _, hostnameRawVal := range hostnameVs { hostnameV := hostnameRawVal.(map[string]interface{}) @@ -856,26 +891,7 @@ func flattenApiManagementHostnameConfigurations(input *[]apimanagement.HostnameC output["key_vault_id"] = *config.KeyVaultID } - // Iterate through old state to find sensitive props not returned by API. - // This must be done in order to avoid state diffs. - // NOTE: this information won't be available during times like Import, so this is a best-effort. - existingHostnames := d.Get("hostname_configuration").([]interface{}) - if len(existingHostnames) > 0 { - v := existingHostnames[0].(map[string]interface{}) - - if valsRaw, ok := v[strings.ToLower(string(config.Type))]; ok { - vals := valsRaw.([]interface{}) - for _, val := range vals { - oldConfig := val.(map[string]interface{}) - - if oldConfig["host_name"] == *config.HostName { - output["certificate_password"] = oldConfig["certificate_password"] - output["certificate"] = oldConfig["certificate"] - } - } - } - } - + var configType string switch strings.ToLower(string(config.Type)) { case strings.ToLower(string(apimanagement.HostnameTypeProxy)): // only set SSL binding for proxy types @@ -883,18 +899,33 @@ func flattenApiManagementHostnameConfigurations(input *[]apimanagement.HostnameC output["default_ssl_binding"] = *config.DefaultSslBinding } proxyResults = append(proxyResults, output) + configType = "proxy" case strings.ToLower(string(apimanagement.HostnameTypeManagement)): managementResults = append(managementResults, output) + configType = "management" case strings.ToLower(string(apimanagement.HostnameTypePortal)): portalResults = append(portalResults, output) + configType = "portal" case strings.ToLower(string(apimanagement.HostnameTypeDeveloperPortal)): developerPortalResults = append(developerPortalResults, output) + configType = "developer_portal" case strings.ToLower(string(apimanagement.HostnameTypeScm)): scmResults = append(scmResults, output) + configType = "scm" + } + + existingHostnames := d.Get("hostname_configuration").([]interface{}) + if len(existingHostnames) > 0 && configType != "" { + v := existingHostnames[0].(map[string]interface{}) + + if valsRaw, ok := v[configType]; ok { + vals := valsRaw.([]interface{}) + azure.CopyCertificateAndPassword(vals, *config.HostName, output) + } } } @@ -1180,54 +1211,6 @@ func flattenApiManagementVirtualNetworkConfiguration(input *apimanagement.Virtua return []interface{}{virtualNetworkConfiguration} } -func apiManagementResourceHostnameSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "host_name": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringIsNotEmpty, - }, - - "key_vault_id": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: azure.ValidateKeyVaultChildIdVersionOptional, - }, - - "certificate": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - ValidateFunc: validation.StringIsNotEmpty, - }, - - "certificate_password": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - ValidateFunc: validation.StringIsNotEmpty, - }, - - "negotiate_client_certificate": { - Type: schema.TypeBool, - Optional: true, - Default: false, - }, - } -} - -func apiManagementResourceHostnameProxySchema() map[string]*schema.Schema { - hostnameSchema := apiManagementResourceHostnameSchema() - - hostnameSchema["default_ssl_binding"] = &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Computed: true, // Azure has certain logic to set this, which we cannot predict - } - - return hostnameSchema -} - func parseApiManagementNilableDictionary(input map[string]*string, key string) bool { log.Printf("Parsing value for %q", key) diff --git a/azurerm/internal/services/apimanagement/parse/api_diagnostic_id_test.go b/azurerm/internal/services/apimanagement/parse/api_diagnostic_id_test.go index d748c67d0846..a6f04cba8498 100644 --- a/azurerm/internal/services/apimanagement/parse/api_diagnostic_id_test.go +++ b/azurerm/internal/services/apimanagement/parse/api_diagnostic_id_test.go @@ -101,7 +101,7 @@ func TestApiManagementApiDiagnosticID(t *testing.T) { } if actual.ServiceName != v.Expected.ServiceName { - t.Fatalf("Expected %q but got %q for Service Name", v.Expected.Name, actual.Name) + t.Fatalf("Expected %q but got %q for Service Name", v.Expected.ServiceName, actual.ServiceName) } if actual.ResourceGroup != v.Expected.ResourceGroup { diff --git a/azurerm/internal/services/apimanagement/parse/api_management_id.go b/azurerm/internal/services/apimanagement/parse/api_management_id.go new file mode 100644 index 000000000000..3a4ec9393255 --- /dev/null +++ b/azurerm/internal/services/apimanagement/parse/api_management_id.go @@ -0,0 +1,31 @@ +package parse + +import ( + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type ApiManagementId struct { + ResourceGroup string + ServiceName string +} + +func ApiManagementID(input string) (*ApiManagementId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + apiManagement := ApiManagementId{ + ResourceGroup: id.ResourceGroup, + } + + if apiManagement.ServiceName, err = id.PopSegment("service"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &apiManagement, nil +} diff --git a/azurerm/internal/services/apimanagement/parse/api_management_id_test.go b/azurerm/internal/services/apimanagement/parse/api_management_id_test.go new file mode 100644 index 000000000000..62399a6b728e --- /dev/null +++ b/azurerm/internal/services/apimanagement/parse/api_management_id_test.go @@ -0,0 +1,71 @@ +package parse + +import "testing" + +func TestApiManagementID(t *testing.T) { + testData := []struct { + Name string + Input string + Expected *ApiManagementId + }{ + { + Name: "Empty", + Input: "", + Expected: nil, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Expected: nil, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Expected: nil, + }, + { + Name: "Resource Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/", + Expected: nil, + }, + { + Name: "Missing Service Name", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ApiManagement/service/", + Expected: nil, + }, + { + Name: "API Management service name", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ApiManagement/service/service1", + Expected: &ApiManagementId{ + ServiceName: "service1", + ResourceGroup: "resGroup1", + }, + }, + { + Name: "Wrong Casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ApiManagement/Service/service1", + Expected: nil, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := ApiManagementID(v.Input) + if err != nil { + if v.Expected == nil { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.ServiceName != v.Expected.ServiceName { + t.Fatalf("Expected %q but got %q for Service Name", v.Expected.ServiceName, actual.ServiceName) + } + + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expected.ResourceGroup, actual.ResourceGroup) + } + } +} diff --git a/azurerm/internal/services/apimanagement/parse/custom_domain_id.go b/azurerm/internal/services/apimanagement/parse/custom_domain_id.go new file mode 100644 index 000000000000..8763c15bc8cd --- /dev/null +++ b/azurerm/internal/services/apimanagement/parse/custom_domain_id.go @@ -0,0 +1,36 @@ +package parse + +import ( + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type ApiManagementCustomDomainId struct { + ResourceGroup string + ServiceName string + Name string +} + +func ApiManagementCustomDomainID(input string) (*ApiManagementCustomDomainId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + logger := ApiManagementCustomDomainId{ + ResourceGroup: id.ResourceGroup, + } + + if logger.ServiceName, err = id.PopSegment("service"); err != nil { + return nil, err + } + + if logger.Name, err = id.PopSegment("customDomains"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &logger, nil +} diff --git a/azurerm/internal/services/apimanagement/parse/custom_domain_id_test.go b/azurerm/internal/services/apimanagement/parse/custom_domain_id_test.go new file mode 100644 index 000000000000..d7d5cb6e48ca --- /dev/null +++ b/azurerm/internal/services/apimanagement/parse/custom_domain_id_test.go @@ -0,0 +1,86 @@ +package parse + +import "testing" + +func TestApiManagementCustomDomainID(t *testing.T) { + testData := []struct { + Name string + Input string + Expected *ApiManagementCustomDomainId + }{ + { + Name: "Empty", + Input: "", + Expected: nil, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Expected: nil, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Expected: nil, + }, + { + Name: "Resource Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/", + Expected: nil, + }, + { + Name: "Missing Service Name", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ApiManagement/service/", + Expected: nil, + }, + { + Name: "Missing Custom Domain", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ApiManagement/service/service1", + Expected: nil, + }, + { + Name: "Missing Custom Domain Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ApiManagement/service/service1/customDomains", + Expected: nil, + }, + { + Name: "Custom Domain ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ApiManagement/service/service1/customDomains/default", + Expected: &ApiManagementCustomDomainId{ + Name: "default", + ServiceName: "service1", + ResourceGroup: "resGroup1", + }, + }, + { + Name: "Wrong Casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ApiManagement/service/service1/CstomDomains/default", + Expected: nil, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := ApiManagementCustomDomainID(v.Input) + if err != nil { + if v.Expected == nil { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + + if actual.ServiceName != v.Expected.ServiceName { + t.Fatalf("Expected %q but got %q for Service Name", v.Expected.Name, actual.Name) + } + + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expected.ResourceGroup, actual.ResourceGroup) + } + } +} diff --git a/azurerm/internal/services/apimanagement/registration.go b/azurerm/internal/services/apimanagement/registration.go index 1c1160925d1e..1b38d6ce0a43 100644 --- a/azurerm/internal/services/apimanagement/registration.go +++ b/azurerm/internal/services/apimanagement/registration.go @@ -44,6 +44,7 @@ func (r Registration) SupportedResources() map[string]*schema.Resource { "azurerm_api_management_authorization_server": resourceArmApiManagementAuthorizationServer(), "azurerm_api_management_backend": resourceArmApiManagementBackend(), "azurerm_api_management_certificate": resourceArmApiManagementCertificate(), + "azurerm_api_management_custom_domain": resourceArmApiManagementCustomDomain(), "azurerm_api_management_diagnostic": resourceArmApiManagementDiagnostic(), "azurerm_api_management_group": resourceArmApiManagementGroup(), "azurerm_api_management_group_user": resourceArmApiManagementGroupUser(), diff --git a/azurerm/internal/services/apimanagement/schema.go b/azurerm/internal/services/apimanagement/schema.go new file mode 100644 index 000000000000..2cfff58efbd8 --- /dev/null +++ b/azurerm/internal/services/apimanagement/schema.go @@ -0,0 +1,58 @@ +package apimanagement + +import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/suppress" +) + +func apiManagementResourceHostnameSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "host_name": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: suppress.CaseDifference, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "key_vault_id": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: azure.ValidateKeyVaultChildIdVersionOptional, + }, + + "certificate": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "certificate_password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "negotiate_client_certificate": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + } +} + +func apiManagementResourceHostnameProxySchema() map[string]*schema.Schema { + hostnameSchema := apiManagementResourceHostnameSchema() + + hostnameSchema["default_ssl_binding"] = &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Computed: true, // Azure has certain logic to set this, which we cannot predict + } + + return hostnameSchema +} diff --git a/azurerm/internal/services/apimanagement/tests/api_management_custom_domain_resource_test.go b/azurerm/internal/services/apimanagement/tests/api_management_custom_domain_resource_test.go new file mode 100644 index 000000000000..9ebb6944f923 --- /dev/null +++ b/azurerm/internal/services/apimanagement/tests/api_management_custom_domain_resource_test.go @@ -0,0 +1,389 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/apimanagement/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMApiManagementCustomDomain_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management_custom_domain", "test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementCustomDomainDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagementCustomDomain_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementCustomDomainExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMApiManagementCustomDomain_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management_custom_domain", "test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementCustomDomainDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagementCustomDomain_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementCustomDomainExists(data.ResourceName), + ), + }, + data.RequiresImportErrorStep(testAccAzureRMApiManagementCustomDomain_requiresImport), + }, + }) +} + +func TestAccAzureRMApiManagementCustomDomain_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management_custom_domain", "test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApiManagementCustomDomainDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApiManagementCustomDomain_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementCustomDomainExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMApiManagementCustomDomain_proxyOnly(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementCustomDomainExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMApiManagementCustomDomain_developerPortalOnly(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementCustomDomainExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMApiManagementCustomDomain_builtinProxyOnly(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApiManagementCustomDomainExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func testCheckAzureRMApiManagementCustomDomainDestroy(s *terraform.State) error { + conn := acceptance.AzureProvider.Meta().(*clients.Client).ApiManagement.ServiceClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_api_management_custom_domain" { + continue + } + + customDomainId := rs.Primary.ID + id, err := parse.ApiManagementCustomDomainID(customDomainId) + if err != nil { + return fmt.Errorf("Error parsing ID %q for API Management Custom Domain: %+v", customDomainId, err) + } + + resp, err := conn.Get(ctx, id.ResourceGroup, id.ServiceName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + + if resp.ServiceProperties != nil && resp.ServiceProperties.HostnameConfigurations != nil && len(*resp.ServiceProperties.HostnameConfigurations) > 0 { + return fmt.Errorf("Bad: Expected there to be no Custom Domains in the hostname_configurations field: %+v", resp.ServiceProperties.HostnameConfigurations) + } + + return err + } + + return nil + } + + return nil +} + +func testCheckAzureRMApiManagementCustomDomainExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acceptance.AzureProvider.Meta().(*clients.Client).ApiManagement.ServiceClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + // Ensure we have enough information in state to look up in API + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + customDomainId := rs.Primary.ID + id, err := parse.ApiManagementCustomDomainID(customDomainId) + if err != nil { + return fmt.Errorf("Error parsing ID %q for API Management Custom Domain: %+v", customDomainId, err) + } + + resp, err := conn.Get(ctx, id.ResourceGroup, id.ServiceName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Custom Domains on API Management Service %q / Resource Group: %q does not exist (because API Management Service %q does not exist)", id.ServiceName, id.ResourceGroup, id.ServiceName) + } + + if resp.ServiceProperties == nil || resp.ServiceProperties.HostnameConfigurations == nil || len(*resp.ServiceProperties.HostnameConfigurations) == 0 { + return fmt.Errorf("Bad: Expected there to be Custom Domains defined in the hostname_configurations field for API Management Service %q / Resource Group: %q", id.ServiceName, id.ResourceGroup) + } + + return fmt.Errorf("Bad: Get on apiManagementCustomDomainsClient: %+v", err) + } + + return nil + } +} + +func testAccAzureRMApiManagementCustomDomain_basic(data acceptance.TestData) string { + template := testAccAzureRMApiManagementCustomDomain_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_api_management_custom_domain" "test" { + api_management_id = azurerm_api_management.test.id + + proxy { + host_name = "${azurerm_api_management.test.name}.azure-api.net" + } + + proxy { + host_name = "api.example.com" + key_vault_id = azurerm_key_vault_certificate.test.secret_id + } + + developer_portal { + host_name = "portal.example.com" + key_vault_id = azurerm_key_vault_certificate.test.secret_id + } +} +`, template) +} + +func testAccAzureRMApiManagementCustomDomain_proxyOnly(data acceptance.TestData) string { + template := testAccAzureRMApiManagementCustomDomain_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_api_management_custom_domain" "test" { + api_management_id = azurerm_api_management.test.id + + proxy { + host_name = "${azurerm_api_management.test.name}.azure-api.net" + } + + proxy { + host_name = "api.example.com" + key_vault_id = azurerm_key_vault_certificate.test.secret_id + } +} +`, template) +} + +func testAccAzureRMApiManagementCustomDomain_developerPortalOnly(data acceptance.TestData) string { + template := testAccAzureRMApiManagementCustomDomain_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_api_management_custom_domain" "test" { + api_management_id = azurerm_api_management.test.id + + proxy { + host_name = "${azurerm_api_management.test.name}.azure-api.net" + } + + developer_portal { + host_name = "portal.example.com" + key_vault_id = azurerm_key_vault_certificate.test.secret_id + } +} +`, template) +} + +func testAccAzureRMApiManagementCustomDomain_builtinProxyOnly(data acceptance.TestData) string { + template := testAccAzureRMApiManagementCustomDomain_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_api_management_custom_domain" "test" { + api_management_id = azurerm_api_management.test.id + + proxy { + host_name = "${azurerm_api_management.test.name}.azure-api.net" + } +} +`, template) +} + +func testAccAzureRMApiManagementCustomDomain_requiresImport(data acceptance.TestData) string { + template := testAccAzureRMApiManagementCustomDomain_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_api_management_custom_domain" "import" { + api_management_id = azurerm_api_management_custom_domain.test.api_management_id + + proxy { + host_name = "${azurerm_api_management.test.name}.azure-api.net" + } + + proxy { + host_name = "api.example.com" + key_vault_id = azurerm_key_vault_certificate.test.secret_id + } + + developer_portal { + host_name = "portal.example.com" + key_vault_id = azurerm_key_vault_certificate.test.secret_id + } +} +`, template) +} + +func testAccAzureRMApiManagementCustomDomain_template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +data "azurerm_client_config" "current" { +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +resource "azurerm_api_management" "test" { + name = "acctestAM-%[1]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" + } +} + +resource "azurerm_key_vault" "test" { + name = "apimkv%[3]s" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + tenant_id = data.azurerm_client_config.current.tenant_id + + sku_name = "standard" + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + certificate_permissions = [ + "create", + "delete", + "get", + "update", + ] + + key_permissions = [ + "create", + "get", + ] + + secret_permissions = [ + "get", + ] + } + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azurerm_api_management.test.identity.0.principal_id + + certificate_permissions = [ + "get", + ] + + secret_permissions = [ + "get", + ] + } +} + +resource "azurerm_key_vault_certificate" "test" { + name = "acctestcert%[3]s" + key_vault_id = azurerm_key_vault.test.id + + certificate_policy { + issuer_parameters { + name = "Self" + } + + key_properties { + exportable = true + key_size = 2048 + key_type = "RSA" + reuse_key = true + } + + lifetime_action { + action { + action_type = "AutoRenew" + } + + trigger { + days_before_expiry = 30 + } + } + + secret_properties { + content_type = "application/x-pkcs12" + } + + x509_certificate_properties { + key_usage = [ + "cRLSign", + "dataEncipherment", + "digitalSignature", + "keyAgreement", + "keyCertSign", + "keyEncipherment", + ] + + subject = "CN=api.example.com" + validity_in_months = 12 + + subject_alternative_names { + dns_names = [ + "api.example.com", + "portal.example.com", + ] + } + } + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} diff --git a/azurerm/internal/services/apimanagement/tests/api_management_data_source_test.go b/azurerm/internal/services/apimanagement/tests/api_management_data_source_test.go index c6c2b106f56b..69eb43450cc8 100644 --- a/azurerm/internal/services/apimanagement/tests/api_management_data_source_test.go +++ b/azurerm/internal/services/apimanagement/tests/api_management_data_source_test.go @@ -29,6 +29,29 @@ func TestAccDataSourceAzureRMApiManagement_basic(t *testing.T) { }) } +func TestAccDataSourceAzureRMApiManagement_identitySystemAssigned(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_api_management", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceApiManagement_identitySystemAssigned(data), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(data.ResourceName, "publisher_email", "pub1@email.com"), + resource.TestCheckResourceAttr(data.ResourceName, "publisher_name", "pub1"), + resource.TestCheckResourceAttr(data.ResourceName, "sku_name", "Developer_1"), + resource.TestCheckResourceAttr(data.ResourceName, "tags.%", "0"), + resource.TestCheckResourceAttrSet(data.ResourceName, "public_ip_addresses.#"), + resource.TestCheckResourceAttr(data.ResourceName, "identity.#", "1"), + resource.TestCheckResourceAttr(data.ResourceName, "identity.0.type", "SystemAssigned"), + ), + }, + }, + }) +} + func TestAccDataSourceAzureRMApiManagement_virtualNetwork(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_api_management", "test") @@ -73,6 +96,36 @@ resource "azurerm_api_management" "test" { resource_group_name = azurerm_resource_group.test.name } +data "azurerm_api_management" "test" { + name = azurerm_api_management.test.name + resource_group_name = azurerm_api_management.test.resource_group_name +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} +func testAccDataSourceApiManagement_identitySystemAssigned(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "amtestRG-%d" + location = "%s" +} + +resource "azurerm_api_management" "test" { + name = "acctestAM-%d" + publisher_name = "pub1" + publisher_email = "pub1@email.com" + sku_name = "Developer_1" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + identity { + type = "SystemAssigned" + } +} + data "azurerm_api_management" "test" { name = azurerm_api_management.test.name resource_group_name = azurerm_api_management.test.resource_group_name diff --git a/website/docs/r/api_management.html.markdown b/website/docs/r/api_management.html.markdown index 4c20071cf791..12e0a7b3b35c 100644 --- a/website/docs/r/api_management.html.markdown +++ b/website/docs/r/api_management.html.markdown @@ -10,6 +10,10 @@ description: |- Manages an API Management Service. +## Disclaimers + +~> **Note:** It's possible to define Custom Domains both within [the `azurerm_api_management` resource](api_management.html) via the `hostname_configurations` block and by using [the `azurerm_api_management_custom_domain` resource](api_management_custom_domain.html). However it's not possible to use both methods to manage Custom Domains within an API Management Service, since there'll be conflicts. + ## Example Usage ```hcl diff --git a/website/docs/r/api_management_custom_domain.html.markdown b/website/docs/r/api_management_custom_domain.html.markdown new file mode 100644 index 000000000000..354fd0f0aea0 --- /dev/null +++ b/website/docs/r/api_management_custom_domain.html.markdown @@ -0,0 +1,171 @@ +--- +subcategory: "API Management" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_api_management_custom_domain" +description: |- + Manages a API Management Custom Domain. +--- + +# azurerm_api_management_custom_domain + +Manages a API Management Custom Domain. + +## Disclaimers + +~> **Note:** It's possible to define Custom Domains both within [the `azurerm_api_management` resource](api_management.html) via the `hostname_configurations` block and by using this resource. However it's not possible to use both methods to manage Custom Domains within an API Management Service, since there will be conflicts. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West US" +} + +resource "azurerm_api_management" "example" { + name = "example-apim" + 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" +} + +resource "azurerm_key_vault_certificate" "example" { + name = "example-certificate" + key_vault_id = azurerm_key_vault.test.id + + certificate_policy { + issuer_parameters { + name = "Self" + } + + key_properties { + exportable = true + key_size = 2048 + key_type = "RSA" + reuse_key = true + } + + lifetime_action { + action { + action_type = "AutoRenew" + } + + trigger { + days_before_expiry = 30 + } + } + + secret_properties { + content_type = "application/x-pkcs12" + } + + x509_certificate_properties { + key_usage = [ + "cRLSign", + "dataEncipherment", + "digitalSignature", + "keyAgreement", + "keyCertSign", + "keyEncipherment", + ] + + subject = "CN=api.example.com" + validity_in_months = 12 + + subject_alternative_names { + dns_names = [ + "api.example.com", + "portal.example.com", + ] + } + } + } +} + +resource "azurerm_api_management_custom_domain" "example" { + api_management_id = azurerm_api_management.example.id + + proxy { + host_name = "api.example.com" + key_vault_id = azurerm_key_vault_certificate.test.secret_id + } + + developer_portal { + host_name = "portal.example.com" + key_vault_id = azurerm_key_vault_certificate.test.secret_id + } +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `api_management_id` - (Required) The ID of the API Management service for which to configure Custom Domains. Changing this forces a new API Management Custom Domain resource to be created. + +--- + +* `developer_portal` - (Optional) One or more `developer_portal` blocks as defined below. + +* `management` - (Optional) One or more `management` blocks as defined below. + +* `portal` - (Optional) One or more `portal` blocks as defined below. + +* `proxy` - (Optional) One or more `proxy` blocks as defined below. + +* `scm` - (Optional) One or more `scm` blocks as defined below. + +--- + +A `developer_portal`, `management`, `portal` or `scm` block supports the following: + +* `host_name` - (Required) The Hostname to use for the corresponding endpoint. + +* `certificate` - (Optional) The Base64 Encoded Certificate. (Mutually exlusive with `key_vault_id`.) + +* `certificate_password` - (Optional) The password associated with the certificate provided above. + +* `key_vault_id` - (Optional) The ID of the Key Vault Secret containing the SSL Certificate, which must be should be of the type application/x-pkcs12. + +* `negotiate_client_certificate` - (Optional) Should Client Certificate Negotiation be enabled for this Hostname? Defaults to false. + +--- + +A `proxy` block supports the following: + +* `host_name` - (Required) The Hostname to use for the API Proxy Endpoint. + +* `certificate` - (Optional) The Base64 Encoded Certificate. (Mutually exlusive with `key_vault_id`.) + +* `certificate_password` - (Optional) The password associated with the certificate provided above. + +* `default_ssl_binding` - (Optional) Is the certificate associated with this Hostname the Default SSL Certificate? This is used when an SNI header isn't specified by a client. Defaults to false. + +* `key_vault_id` - (Optional) The ID of the Key Vault Secret containing the SSL Certificate, which must be should be of the type application/x-pkcs12. + +* `negotiate_client_certificate` - (Optional) Should Client Certificate Negotiation be enabled for this Hostname? Defaults to false. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the API Management Custom Domain. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the API Management Custom Domain. +* `read` - (Defaults to 5 minutes) Used when retrieving the API Management Custom Domain. +* `update` - (Defaults to 30 minutes) Used when updating the API Management Custom Domain. +* `delete` - (Defaults to 30 minutes) Used when deleting the API Management Custom Domain. + +## Import + +API Management Custom Domains can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_api_management_custom_domain.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.ApiManagement/service/instance1 +```