diff --git a/azurerm/helpers/azure/frontdoor_custom_https_configuration.go b/azurerm/helpers/azure/frontdoor_custom_https_configuration.go new file mode 100644 index 000000000000..0715272a3db1 --- /dev/null +++ b/azurerm/helpers/azure/frontdoor_custom_https_configuration.go @@ -0,0 +1,94 @@ +package azure + +import ( + "fmt" + + "github.com/Azure/azure-sdk-for-go/services/frontdoor/mgmt/2020-01-01/frontdoor" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func SchemaFrontdoorCustomHttpsConfiguration() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "certificate_source": { + Type: schema.TypeString, + Optional: true, + Default: string(frontdoor.CertificateSourceFrontDoor), + ValidateFunc: validation.StringInSlice([]string{ + string(frontdoor.CertificateSourceAzureKeyVault), + string(frontdoor.CertificateSourceFrontDoor), + }, false), + }, + "minimum_tls_version": { + Type: schema.TypeString, + Computed: true, + }, + "provisioning_state": { + Type: schema.TypeString, + Computed: true, + }, + "provisioning_substate": { + Type: schema.TypeString, + Computed: true, + }, + // NOTE: None of these attributes are valid if + // certificate_source is set to FrontDoor + "azure_key_vault_certificate_secret_name": { + Type: schema.TypeString, + Optional: true, + }, + "azure_key_vault_certificate_secret_version": { + Type: schema.TypeString, + Optional: true, + }, + "azure_key_vault_certificate_vault_id": { + Type: schema.TypeString, + Optional: true, + }, + } +} + +func FlattenArmFrontDoorCustomHttpsConfiguration(input *frontdoor.FrontendEndpoint, output map[string]interface{}, resourceGroup string) error { + if input == nil { + return fmt.Errorf("cannot read Front Door Frontend Endpoint (Resource Group %q): endpoint is empty", resourceGroup) + } + + customHttpsConfiguration := make([]interface{}, 0) + chc := make(map[string]interface{}) + + if properties := input.FrontendEndpointProperties; properties != nil { + if properties.CustomHTTPSConfiguration != nil { + customHTTPSConfiguration := properties.CustomHTTPSConfiguration + if customHTTPSConfiguration.CertificateSource == frontdoor.CertificateSourceAzureKeyVault { + if kvcsp := customHTTPSConfiguration.KeyVaultCertificateSourceParameters; kvcsp != nil { + chc["certificate_source"] = string(frontdoor.CertificateSourceAzureKeyVault) + chc["azure_key_vault_certificate_vault_id"] = *kvcsp.Vault.ID + chc["azure_key_vault_certificate_secret_name"] = *kvcsp.SecretName + chc["azure_key_vault_certificate_secret_version"] = *kvcsp.SecretVersion + } + } else { + chc["certificate_source"] = string(frontdoor.CertificateSourceFrontDoor) + } + + chc["minimum_tls_version"] = string(customHTTPSConfiguration.MinimumTLSVersion) + + if provisioningState := properties.CustomHTTPSProvisioningState; provisioningState != "" { + chc["provisioning_state"] = provisioningState + if provisioningState == frontdoor.CustomHTTPSProvisioningStateEnabled || provisioningState == frontdoor.CustomHTTPSProvisioningStateEnabling { + output["custom_https_provisioning_enabled"] = true + + if provisioningSubstate := properties.CustomHTTPSProvisioningSubstate; provisioningSubstate != "" { + chc["provisioning_substate"] = provisioningSubstate + } + } else { + output["custom_https_provisioning_enabled"] = false + } + + customHttpsConfiguration = append(customHttpsConfiguration, chc) + output["custom_https_configuration"] = customHttpsConfiguration + } + } + } + + return nil +} diff --git a/azurerm/internal/services/frontdoor/frontdoor_custom_https_configuration_resource.go b/azurerm/internal/services/frontdoor/frontdoor_custom_https_configuration_resource.go new file mode 100644 index 000000000000..d60e14848c89 --- /dev/null +++ b/azurerm/internal/services/frontdoor/frontdoor_custom_https_configuration_resource.go @@ -0,0 +1,323 @@ +package frontdoor + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/services/frontdoor/mgmt/2020-01-01/frontdoor" + "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/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/locks" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/frontdoor/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmFrontDoorCustomHttpsConfiguration() *schema.Resource { + return &schema.Resource{ + Create: resourceArmFrontDoorCustomHttpsConfigurationCreateUpdate, + Read: resourceArmFrontDoorCustomHttpsConfigurationRead, + Update: resourceArmFrontDoorCustomHttpsConfigurationCreateUpdate, + Delete: resourceArmFrontDoorCustomHttpsConfigurationDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(6 * time.Hour), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(6 * time.Hour), + Delete: schema.DefaultTimeout(6 * time.Hour), + }, + + Schema: map[string]*schema.Schema{ + "frontend_endpoint_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: azure.ValidateResourceID, + }, + + "custom_https_provisioning_enabled": { + Type: schema.TypeBool, + Required: true, + }, + + "resource_group_name": azure.SchemaResourceGroupName(), + + "custom_https_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: azure.SchemaFrontdoorCustomHttpsConfiguration(), + }, + }, + }, + + CustomizeDiff: func(d *schema.ResourceDiff, v interface{}) error { + if err := validate.FrontdoorCustomHttpsSettings(d); err != nil { + return fmt.Errorf("creating Front Door Custom Https Configuration for endpoint %q (Resource Group %q): %+v", d.Get("frontend_endpoint_id").(string), d.Get("resource_group_name").(string), err) + } + + return nil + }, + } +} + +func resourceArmFrontDoorCustomHttpsConfigurationCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Frontdoor.FrontDoorsFrontendClient + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + err, resourceGroup, frontDoorName, frontendEndpointName := frontDoorFrontendEndpointReadProps(d) + if err != nil { + return err + } + + resp, err := client.Get(ctx, resourceGroup, frontDoorName, frontendEndpointName) + if err != nil { + return fmt.Errorf("reading Front Door Endpoint %q (Resource Group %q): %+v", frontendEndpointName, resourceGroup, err) + } + + // This is because azure doesn't have an 'id' for a custom https configuration + // In order to compensate for this and allow importing of this resource we are artificially + // creating an identity for a custom https configuration object + var resourceId string + if id := resp.ID; id != nil && *id != "" { + resourceId = fmt.Sprintf("%s/customHttpsConfiguration/%s", *id, frontendEndpointName) + } else { + return fmt.Errorf("unable to retrieve Front Door Endpoint %q (Resource Group %q) ID", frontendEndpointName, resourceGroup) + } + + customHttpsProvisioningEnabled := d.Get("custom_https_provisioning_enabled").(bool) + customHttpsConfigurationNew := d.Get("custom_https_configuration").([]interface{}) + err = resourceArmFrontDoorFrontendEndpointCustomHttpsConfigurationUpdate(ctx, *resp.ID, customHttpsProvisioningEnabled, frontDoorName, frontendEndpointName, resourceGroup, resp.CustomHTTPSProvisioningState, resp.CustomHTTPSConfiguration, customHttpsConfigurationNew, meta) + if err != nil { + return fmt.Errorf("unable to update Custom HTTPS configuration for Frontend Endpoint %q (Resource Group %q): %+v", frontendEndpointName, resourceGroup, err) + } + + resp, err = client.Get(ctx, resourceGroup, frontDoorName, frontendEndpointName) + if err != nil { + return fmt.Errorf("retreving Front Door Endpoint %q (Resource Group %q): %+v", frontendEndpointName, resourceGroup, err) + } + + if resp.ID == nil { + return fmt.Errorf("cannot read Front Door Endpoint %q (Resource Group %q) ID", frontendEndpointName, resourceGroup) + } + + if d.IsNewResource() { + d.SetId(resourceId) + } + + return nil +} + +func resourceArmFrontDoorCustomHttpsConfigurationRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Frontdoor.FrontDoorsFrontendClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + err, resourceGroup, frontDoorName, frontendEndpointName := frontDoorCustomHttpsConfigurationReadProps(d) + if err != nil { + return err + } + + resp, err := client.Get(ctx, resourceGroup, frontDoorName, frontendEndpointName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] Front Door Endpoint %q does not exist - removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("reading Front Door Endpoint %q (Resource Group %q): %+v", frontendEndpointName, resourceGroup, err) + } + + d.Set("frontend_endpoint_id", resp.ID) + + if resp.Name != nil { + output := make(map[string]interface{}) + if err := azure.FlattenArmFrontDoorCustomHttpsConfiguration(&resp, output, *resp.Name); err != nil { + return fmt.Errorf("flattening `frontend_endpoint/custom_https_configuration`: %s", *resp.Name) + } + if err := d.Set("custom_https_configuration", output["custom_https_configuration"]); err != nil { + return fmt.Errorf("setting `custom_https_configuration`: %+v", err) + } + if err := d.Set("custom_https_provisioning_enabled", output["custom_https_provisioning_enabled"]); err != nil { + return fmt.Errorf("setting `custom_https_provisioning_enabled`: %+v", err) + } + } else { + return fmt.Errorf("flattening `frontend_endpoint` `custom_https_configuration`: Unable to read Frontend Endpoint Name") + } + + return nil +} + +func resourceArmFrontDoorCustomHttpsConfigurationDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Frontdoor.FrontDoorsFrontendClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + err, resourceGroup, frontDoorName, frontendEndpointName := frontDoorCustomHttpsConfigurationReadProps(d) + if err != nil { + return err + } + + resp, err := client.Get(ctx, resourceGroup, frontDoorName, frontendEndpointName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + return fmt.Errorf("reading Front Door Endpoint %q (Resource Group %q): %+v", frontendEndpointName, resourceGroup, err) + } + + customHttpsConfigurationNew := make([]interface{}, 0) + err = resourceArmFrontDoorFrontendEndpointCustomHttpsConfigurationUpdate(ctx, *resp.ID, false, frontDoorName, frontendEndpointName, resourceGroup, resp.CustomHTTPSProvisioningState, resp.CustomHTTPSConfiguration, customHttpsConfigurationNew, meta) + if err != nil { + return fmt.Errorf("unable to disable Custom HTTPS configuration for Frontend Endpoint %q (Resource Group %q): %+v", frontendEndpointName, resourceGroup, err) + } + + return nil +} + +func frontDoorFrontendEndpointReadProps(d *schema.ResourceData) (err error, resourceGroup string, frontDoorName string, frontendEndpointName string) { + id, err := azure.ParseAzureResourceID(d.Get("frontend_endpoint_id").(string)) + if err != nil { + return err, "", "", "" + } + return frontDoorReadPropsFromId(id) +} + +func frontDoorCustomHttpsConfigurationReadProps(d *schema.ResourceData) (err error, resourceGroup string, frontDoorName string, frontendEndpointName string) { + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err, "", "", "" + } + return frontDoorReadPropsFromId(id) +} + +func frontDoorReadPropsFromId(id *azure.ResourceID) (err error, resourceGroup string, frontDoorName string, frontendEndpointName string) { + resourceGroup = id.ResourceGroup + frontDoorName = id.Path["frontdoors"] + // Link to issue: https://github.com/Azure/azure-sdk-for-go/issues/6762 + if frontDoorName == "" { + frontDoorName = id.Path["Frontdoors"] + } + frontendEndpointName = id.Path["frontendendpoints"] + if frontendEndpointName == "" { + frontDoorName = id.Path["FrontendEndpoints"] + } + + return nil, resourceGroup, frontDoorName, frontendEndpointName +} + +func resourceArmFrontDoorFrontendEndpointCustomHttpsConfigurationUpdate(ctx context.Context, frontendEndpointId string, customHttpsProvisioningEnabled bool, frontDoorName string, frontendEndpointName string, resourceGroup string, provisioningState frontdoor.CustomHTTPSProvisioningState, customHTTPSConfigurationCurrent *frontdoor.CustomHTTPSConfiguration, customHttpsConfigurationNew []interface{}, meta interface{}) error { + // Locking to prevent parallel changes causing issues + locks.ByID(frontendEndpointId) + defer locks.UnlockByID(frontendEndpointId) + + if provisioningState != "" { + // Check to see if we are going to change the CustomHTTPSProvisioningState, if so check to + // see if its current state is configurable, if not return an error... + if customHttpsProvisioningEnabled != NormalizeCustomHTTPSProvisioningStateToBool(provisioningState) { + if err := IsFrontDoorFrontendEndpointConfigurable(provisioningState, customHttpsProvisioningEnabled, frontendEndpointName, resourceGroup); err != nil { + return err + } + } + + if customHttpsProvisioningEnabled { + // Build a custom Https configuration based off the config file to send to the enable call + // NOTE: I do not need to check to see if this exists since I already do that in the validation code + customHTTPSConfiguration := customHttpsConfigurationNew[0].(map[string]interface{}) + minTLSVersion := frontdoor.OneFullStopTwo // Default to TLS 1.2 + if httpsConfig := customHTTPSConfigurationCurrent; httpsConfig != nil { + minTLSVersion = httpsConfig.MinimumTLSVersion + } + customHTTPSConfigurationUpdate := makeCustomHttpsConfiguration(customHTTPSConfiguration, minTLSVersion) + if provisioningState == frontdoor.CustomHTTPSProvisioningStateDisabled || customHTTPSConfigurationUpdate != *customHTTPSConfigurationCurrent { + // Enable Custom Domain HTTPS for the Frontend Endpoint + if err := resourceArmFrontDoorFrontendEndpointEnableHttpsProvisioning(ctx, true, frontDoorName, frontendEndpointName, resourceGroup, customHTTPSConfigurationUpdate, meta); err != nil { + return fmt.Errorf("unable to enable/update Custom Domain HTTPS for Frontend Endpoint %q (Resource Group %q): %+v", frontendEndpointName, resourceGroup, err) + } + } + } else if !customHttpsProvisioningEnabled && provisioningState == frontdoor.CustomHTTPSProvisioningStateEnabled { + // Disable Custom Domain HTTPS for the Frontend Endpoint + if err := resourceArmFrontDoorFrontendEndpointEnableHttpsProvisioning(ctx, false, frontDoorName, frontendEndpointName, resourceGroup, frontdoor.CustomHTTPSConfiguration{}, meta); err != nil { + return fmt.Errorf("unable to disable Custom Domain HTTPS for Frontend Endpoint %q (Resource Group %q): %+v", frontendEndpointName, resourceGroup, err) + } + } + } + + return nil +} + +func resourceArmFrontDoorFrontendEndpointEnableHttpsProvisioning(ctx context.Context, enableCustomHttpsProvisioning bool, frontDoorName string, frontendEndpointName string, resourceGroup string, customHTTPSConfiguration frontdoor.CustomHTTPSConfiguration, meta interface{}) error { + client := meta.(*clients.Client).Frontdoor.FrontDoorsFrontendClient + + if enableCustomHttpsProvisioning { + future, err := client.EnableHTTPS(ctx, resourceGroup, frontDoorName, frontendEndpointName, customHTTPSConfiguration) + + if err != nil { + return fmt.Errorf("enabling Custom Domain HTTPS for Frontend Endpoint: %+v", err) + } + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting to enable Custom Domain HTTPS for Frontend Endpoint: %+v", err) + } + } else { + future, err := client.DisableHTTPS(ctx, resourceGroup, frontDoorName, frontendEndpointName) + + if err != nil { + return fmt.Errorf("disabling Custom Domain HTTPS for Frontend Endpoint: %+v", err) + } + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + // If the endpoint does not exist but this is not a new resource, the custom https + // configuration which previously existed was deleted with the endpoint, so reflect + // that in state. + resp, err := client.Get(ctx, resourceGroup, frontDoorName, frontendEndpointName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + } + return fmt.Errorf("waiting to disable Custom Domain HTTPS for Frontend Endpoint: %+v", err) + } + } + + return nil +} + +func makeCustomHttpsConfiguration(customHttpsConfiguration map[string]interface{}, minTLSVersion frontdoor.MinimumTLSVersion) frontdoor.CustomHTTPSConfiguration { + // https://github.com/Azure/azure-sdk-for-go/issues/6882 + defaultProtocolType := "ServerNameIndication" + + customHTTPSConfigurationUpdate := frontdoor.CustomHTTPSConfiguration{ + ProtocolType: &defaultProtocolType, + MinimumTLSVersion: minTLSVersion, + } + + if customHttpsConfiguration["certificate_source"].(string) == "AzureKeyVault" { + vaultSecret := customHttpsConfiguration["azure_key_vault_certificate_secret_name"].(string) + vaultVersion := customHttpsConfiguration["azure_key_vault_certificate_secret_version"].(string) + vaultId := customHttpsConfiguration["azure_key_vault_certificate_vault_id"].(string) + + customHTTPSConfigurationUpdate.CertificateSource = frontdoor.CertificateSourceAzureKeyVault + customHTTPSConfigurationUpdate.KeyVaultCertificateSourceParameters = &frontdoor.KeyVaultCertificateSourceParameters{ + Vault: &frontdoor.KeyVaultCertificateSourceParametersVault{ + ID: utils.String(vaultId), + }, + SecretName: utils.String(vaultSecret), + SecretVersion: utils.String(vaultVersion), + } + } else { + customHTTPSConfigurationUpdate.CertificateSource = frontdoor.CertificateSourceFrontDoor + customHTTPSConfigurationUpdate.CertificateSourceParameters = &frontdoor.CertificateSourceParameters{ + CertificateType: frontdoor.Dedicated, + } + } + + return customHTTPSConfigurationUpdate +} diff --git a/azurerm/internal/services/frontdoor/frontdoor_resource.go b/azurerm/internal/services/frontdoor/frontdoor_resource.go index b3c9f2561441..0bd1fc298e38 100644 --- a/azurerm/internal/services/frontdoor/frontdoor_resource.go +++ b/azurerm/internal/services/frontdoor/frontdoor_resource.go @@ -32,6 +32,8 @@ func resourceArmFrontDoor() *schema.Resource { State: schema.ImportStatePassthrough, }, + SchemaVersion: 1, + Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(6 * time.Hour), Read: schema.DefaultTimeout(5 * time.Minute), @@ -423,55 +425,23 @@ func resourceArmFrontDoor() *schema.Resource { Default: 0, }, "custom_https_provisioning_enabled": { - Type: schema.TypeBool, - Required: true, + Type: schema.TypeBool, + Optional: true, + Computed: true, + Deprecated: "Deprecated in favour of `azurerm_frontdoor_custom_https_configuration` resource", }, "web_application_firewall_policy_link_id": { Type: schema.TypeString, Optional: true, }, "custom_https_configuration": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Deprecated: "Deprecated in favour of `azurerm_frontdoor_custom_https_configuration` resource", Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "certificate_source": { - Type: schema.TypeString, - Optional: true, - Default: string(frontdoor.CertificateSourceFrontDoor), - ValidateFunc: validation.StringInSlice([]string{ - string(frontdoor.CertificateSourceAzureKeyVault), - string(frontdoor.CertificateSourceFrontDoor), - }, false), - }, - "minimum_tls_version": { - Type: schema.TypeString, - Computed: true, - }, - "provisioning_state": { - Type: schema.TypeString, - Computed: true, - }, - "provisioning_substate": { - Type: schema.TypeString, - Computed: true, - }, - // NOTE: None of these attributes are valid if - // certificate_source is set to FrontDoor - "azure_key_vault_certificate_secret_name": { - Type: schema.TypeString, - Optional: true, - }, - "azure_key_vault_certificate_secret_version": { - Type: schema.TypeString, - Optional: true, - }, - "azure_key_vault_certificate_vault_id": { - Type: schema.TypeString, - Optional: true, - }, - }, + Schema: azure.SchemaFrontdoorCustomHttpsConfiguration(), }, }, }, @@ -601,35 +571,10 @@ func resourceArmFrontDoorCreateUpdate(d *schema.ResourceData, meta interface{}) } if properties := resp.FrontendEndpointProperties; properties != nil { - if provisioningState := properties.CustomHTTPSProvisioningState; provisioningState != "" { - // Check to see if we are going to change the CustomHTTPSProvisioningState, if so check to - // see if its current state is configurable, if not return an error... - if customHttpsProvisioningEnabled != NormalizeCustomHTTPSProvisioningStateToBool(provisioningState) { - if err := IsFrontDoorFrontendEndpointConfigurable(provisioningState, customHttpsProvisioningEnabled, frontendEndpointName, resourceGroup); err != nil { - return err - } - } - - if customHttpsProvisioningEnabled && provisioningState == frontdoor.CustomHTTPSProvisioningStateDisabled { - // Build a custom Https configuration based off the config file to send to the enable call - // NOTE: I do not need to check to see if this exists since I already do that in the validation code - chc := frontendEndpoint["custom_https_configuration"].([]interface{}) - customHTTPSConfiguration := chc[0].(map[string]interface{}) - minTLSVersion := frontdoor.OneFullStopTwo // Default to TLS 1.2 - if httpsConfig := properties.CustomHTTPSConfiguration; httpsConfig != nil { - minTLSVersion = httpsConfig.MinimumTLSVersion - } - customHTTPSConfigurationUpdate := makeCustomHttpsConfiguration(customHTTPSConfiguration, minTLSVersion) - // Enable Custom Domain HTTPS for the Frontend Endpoint - if err := resourceArmFrontDoorFrontendEndpointEnableHttpsProvisioning(ctx, true, name, frontendEndpointName, resourceGroup, customHTTPSConfigurationUpdate, meta); err != nil { - return fmt.Errorf("Unable enable Custom Domain HTTPS for Frontend Endpoint %q (Resource Group %q): %+v", frontendEndpointName, resourceGroup, err) - } - } else if !customHttpsProvisioningEnabled && provisioningState == frontdoor.CustomHTTPSProvisioningStateEnabled { - // Disable Custom Domain HTTPS for the Frontend Endpoint - if err := resourceArmFrontDoorFrontendEndpointEnableHttpsProvisioning(ctx, false, name, frontendEndpointName, resourceGroup, frontdoor.CustomHTTPSConfiguration{}, meta); err != nil { - return fmt.Errorf("Unable to disable Custom Domain HTTPS for Frontend Endpoint %q (Resource Group %q): %+v", frontendEndpointName, resourceGroup, err) - } - } + customHttpsConfigurationNew := frontendEndpoint["custom_https_configuration"].([]interface{}) + err := resourceArmFrontDoorFrontendEndpointCustomHttpsConfigurationUpdate(ctx, *resp.ID, customHttpsProvisioningEnabled, name, frontendEndpointName, resourceGroup, properties.CustomHTTPSProvisioningState, properties.CustomHTTPSConfiguration, customHttpsConfigurationNew, meta) + if err != nil { + return fmt.Errorf("unable to update Custom HTTPS configuration for Frontend Endpoint %q (Resource Group %q): %+v", frontendEndpointName, resourceGroup, err) } } } @@ -637,32 +582,6 @@ func resourceArmFrontDoorCreateUpdate(d *schema.ResourceData, meta interface{}) return resourceArmFrontDoorRead(d, meta) } -func resourceArmFrontDoorFrontendEndpointEnableHttpsProvisioning(ctx context.Context, enableCustomHttpsProvisioning bool, frontDoorName string, frontendEndpointName string, resourceGroup string, customHTTPSConfiguration frontdoor.CustomHTTPSConfiguration, meta interface{}) error { - client := meta.(*clients.Client).Frontdoor.FrontDoorsFrontendClient - - if enableCustomHttpsProvisioning { - future, err := client.EnableHTTPS(ctx, resourceGroup, frontDoorName, frontendEndpointName, customHTTPSConfiguration) - - if err != nil { - return fmt.Errorf("enabling Custom Domain HTTPS for Frontend Endpoint: %+v", err) - } - if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { - return fmt.Errorf("waiting to enable Custom Domain HTTPS for Frontend Endpoint: %+v", err) - } - } else { - future, err := client.DisableHTTPS(ctx, resourceGroup, frontDoorName, frontendEndpointName) - - if err != nil { - return fmt.Errorf("disabling Custom Domain HTTPS for Frontend Endpoint: %+v", err) - } - if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { - return fmt.Errorf("waiting to disable Custom Domain HTTPS for Frontend Endpoint: %+v", err) - } - } - - return nil -} - func resourceArmFrontDoorRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*clients.Client).Frontdoor.FrontDoorsClient ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) @@ -715,7 +634,7 @@ func resourceArmFrontDoorRead(d *schema.ResourceData, meta interface{}) error { if frontendEndpoints := properties.FrontendEndpoints; frontendEndpoints != nil { if resp.Name != nil { - if frontDoorFrontendEndpoints, err := flattenArmFrontDoorFrontendEndpoint(ctx, frontendEndpoints, resourceGroup, *resp.Name, meta); frontDoorFrontendEndpoints != nil { + if frontDoorFrontendEndpoints, err := flattenArmFrontDoorFrontendEndpoints(ctx, frontendEndpoints, resourceGroup, *resp.Name, meta); frontDoorFrontendEndpoints != nil { if err := d.Set("frontend_endpoint", frontDoorFrontendEndpoints); err != nil { return fmt.Errorf("setting `frontend_endpoint`: %+v", err) } @@ -1268,7 +1187,7 @@ func flattenArmFrontDoorBackend(input *[]frontdoor.Backend) []interface{} { return output } -func flattenArmFrontDoorFrontendEndpoint(ctx context.Context, input *[]frontdoor.FrontendEndpoint, resourceGroup string, frontDoorName string, meta interface{}) ([]interface{}, error) { +func flattenArmFrontDoorFrontendEndpoints(ctx context.Context, input *[]frontdoor.FrontendEndpoint, resourceGroup string, frontDoorName string, meta interface{}) ([]interface{}, error) { if input == nil { return make([]interface{}, 0), fmt.Errorf("cannot read Front Door Frontend Endpoint (Resource Group %q): slice is empty", resourceGroup) } @@ -1276,79 +1195,64 @@ func flattenArmFrontDoorFrontendEndpoint(ctx context.Context, input *[]frontdoor output := make([]interface{}, 0) for _, v := range *input { - result := make(map[string]interface{}) - customHttpsConfiguration := make([]interface{}, 0) - chc := make(map[string]interface{}) + result, err := flattenArmFrontDoorFrontendEndpoint(ctx, &v, resourceGroup, frontDoorName, meta) + if err != nil { + return make([]interface{}, 0), fmt.Errorf("retrieving Front Door Frontend Endpoint Custom HTTPS Configuration %q (Resource Group %q): %+v", *v.Name, resourceGroup, err) + } - if name := v.Name; name != nil { - result["name"] = *name + output = append(output, result) + } - // Need to call frontEndEndpointClient here to get customConfiguration information from that client - // because the information is hidden from the main frontDoorClient "by design"... - client := meta.(*clients.Client).Frontdoor.FrontDoorsFrontendClient + return output, nil +} - resp, err := client.Get(ctx, resourceGroup, frontDoorName, *name) - if err != nil { - return make([]interface{}, 0), fmt.Errorf("retrieving Front Door Frontend Endpoint Custom HTTPS Configuration %q (Resource Group %q): %+v", *name, resourceGroup, err) - } - if resp.ID == nil { - return make([]interface{}, 0), fmt.Errorf("cannot read Front Door Frontend Endpoint Custom HTTPS Configuration %q (Resource Group %q) ID", *name, resourceGroup) - } +func flattenArmFrontDoorFrontendEndpoint(ctx context.Context, input *frontdoor.FrontendEndpoint, resourceGroup string, frontDoorName string, meta interface{}) (map[string]interface{}, error) { + if input == nil { + return make(map[string]interface{}), fmt.Errorf("cannot read Front Door Frontend Endpoint (Resource Group %q): endpoint is empty", resourceGroup) + } - result["id"] = resp.ID + output := make(map[string]interface{}) - if properties := resp.FrontendEndpointProperties; properties != nil { - if hostName := properties.HostName; hostName != nil { - result["host_name"] = *hostName - } + if name := input.Name; name != nil { + output["name"] = *name - if sessionAffinityEnabled := properties.SessionAffinityEnabledState; sessionAffinityEnabled != "" { - result["session_affinity_enabled"] = sessionAffinityEnabled == frontdoor.SessionAffinityEnabledStateEnabled - } + // Need to call frontEndEndpointClient here to get customConfiguration information from that client + // because the information is hidden from the main frontDoorClient "by design"... + client := meta.(*clients.Client).Frontdoor.FrontDoorsFrontendClient - if sessionAffinityTtlSeconds := properties.SessionAffinityTTLSeconds; sessionAffinityTtlSeconds != nil { - result["session_affinity_ttl_seconds"] = *sessionAffinityTtlSeconds - } + resp, err := client.Get(ctx, resourceGroup, frontDoorName, *name) + if err != nil { + return make(map[string]interface{}), fmt.Errorf("retrieving Front Door Frontend Endpoint Custom HTTPS Configuration %q (Resource Group %q): %+v", *name, resourceGroup, err) + } + if resp.ID == nil { + return make(map[string]interface{}), fmt.Errorf("cannot read Front Door Frontend Endpoint Custom HTTPS Configuration %q (Resource Group %q) ID", *name, resourceGroup) + } - if waf := properties.WebApplicationFirewallPolicyLink; waf != nil { - result["web_application_firewall_policy_link_id"] = *waf.ID - } + output["id"] = resp.ID - if properties.CustomHTTPSConfiguration != nil { - customHTTPSConfiguration := properties.CustomHTTPSConfiguration - if customHTTPSConfiguration.CertificateSource == frontdoor.CertificateSourceAzureKeyVault { - if kvcsp := customHTTPSConfiguration.KeyVaultCertificateSourceParameters; kvcsp != nil { - chc["certificate_source"] = string(frontdoor.CertificateSourceAzureKeyVault) - chc["azure_key_vault_certificate_vault_id"] = *kvcsp.Vault.ID - chc["azure_key_vault_certificate_secret_name"] = *kvcsp.SecretName - chc["azure_key_vault_certificate_secret_version"] = *kvcsp.SecretVersion - } - } else { - chc["certificate_source"] = string(frontdoor.CertificateSourceFrontDoor) - } + if props := resp.FrontendEndpointProperties; props != nil { + if hostName := props.HostName; hostName != nil { + output["host_name"] = *hostName + } - chc["minimum_tls_version"] = string(customHTTPSConfiguration.MinimumTLSVersion) + if sessionAffinityEnabled := props.SessionAffinityEnabledState; sessionAffinityEnabled != "" { + output["session_affinity_enabled"] = sessionAffinityEnabled == frontdoor.SessionAffinityEnabledStateEnabled + } - if provisioningState := properties.CustomHTTPSProvisioningState; provisioningState != "" { - chc["provisioning_state"] = provisioningState - if provisioningState == frontdoor.CustomHTTPSProvisioningStateEnabled || provisioningState == frontdoor.CustomHTTPSProvisioningStateEnabling { - result["custom_https_provisioning_enabled"] = true + if sessionAffinityTtlSeconds := props.SessionAffinityTTLSeconds; sessionAffinityTtlSeconds != nil { + output["session_affinity_ttl_seconds"] = *sessionAffinityTtlSeconds + } - if provisioningSubstate := properties.CustomHTTPSProvisioningSubstate; provisioningSubstate != "" { - chc["provisioning_substate"] = provisioningSubstate - } - } else { - result["custom_https_provisioning_enabled"] = false - } + if waf := props.WebApplicationFirewallPolicyLink; waf != nil { + output["web_application_firewall_policy_link_id"] = *waf.ID + } - customHttpsConfiguration = append(customHttpsConfiguration, chc) - result["custom_https_configuration"] = customHttpsConfiguration - } + if props.CustomHTTPSConfiguration != nil { + if err := azure.FlattenArmFrontDoorCustomHttpsConfiguration(&resp, output, *name); err != nil { + return nil, fmt.Errorf("setting `custom_https_configuration`: %+v", err) } } } - - output = append(output, result) } return output, nil @@ -1577,35 +1481,3 @@ func flattenArmFrontDoorFrontendEndpointsSubResources(input *[]frontdoor.SubReso return output } - -func makeCustomHttpsConfiguration(customHttpsConfiguration map[string]interface{}, minTLSVersion frontdoor.MinimumTLSVersion) frontdoor.CustomHTTPSConfiguration { - // https://github.com/Azure/azure-sdk-for-go/issues/6882 - defaultProtocolType := "ServerNameIndication" - - customHTTPSConfigurationUpdate := frontdoor.CustomHTTPSConfiguration{ - ProtocolType: &defaultProtocolType, - MinimumTLSVersion: minTLSVersion, - } - - if customHttpsConfiguration["certificate_source"].(string) == "AzureKeyVault" { - vaultSecret := customHttpsConfiguration["azure_key_vault_certificate_secret_name"].(string) - vaultVersion := customHttpsConfiguration["azure_key_vault_certificate_secret_version"].(string) - vaultId := customHttpsConfiguration["azure_key_vault_certificate_vault_id"].(string) - - customHTTPSConfigurationUpdate.CertificateSource = frontdoor.CertificateSourceAzureKeyVault - customHTTPSConfigurationUpdate.KeyVaultCertificateSourceParameters = &frontdoor.KeyVaultCertificateSourceParameters{ - Vault: &frontdoor.KeyVaultCertificateSourceParametersVault{ - ID: utils.String(vaultId), - }, - SecretName: utils.String(vaultSecret), - SecretVersion: utils.String(vaultVersion), - } - } else { - customHTTPSConfigurationUpdate.CertificateSource = frontdoor.CertificateSourceFrontDoor - customHTTPSConfigurationUpdate.CertificateSourceParameters = &frontdoor.CertificateSourceParameters{ - CertificateType: frontdoor.Dedicated, - } - } - - return customHTTPSConfigurationUpdate -} diff --git a/azurerm/internal/services/frontdoor/registration.go b/azurerm/internal/services/frontdoor/registration.go index bccd7cc0fd54..e2a62bdc11a6 100644 --- a/azurerm/internal/services/frontdoor/registration.go +++ b/azurerm/internal/services/frontdoor/registration.go @@ -28,6 +28,7 @@ func (r Registration) SupportedDataSources() map[string]*schema.Resource { // SupportedResources returns the supported Resources supported by this Service func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ - "azurerm_frontdoor": resourceArmFrontDoor(), - "azurerm_frontdoor_firewall_policy": resourceArmFrontDoorFirewallPolicy()} + "azurerm_frontdoor": resourceArmFrontDoor(), + "azurerm_frontdoor_firewall_policy": resourceArmFrontDoorFirewallPolicy(), + "azurerm_frontdoor_custom_https_configuration": resourceArmFrontDoorCustomHttpsConfiguration()} } diff --git a/azurerm/internal/services/frontdoor/tests/frontdoor_custom_https_configuration_resource_test.go b/azurerm/internal/services/frontdoor/tests/frontdoor_custom_https_configuration_resource_test.go new file mode 100644 index 000000000000..e8dbd654d79e --- /dev/null +++ b/azurerm/internal/services/frontdoor/tests/frontdoor_custom_https_configuration_resource_test.go @@ -0,0 +1,170 @@ +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/helpers/azure" + "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/utils" +) + +func TestAccAzureRMFrontDoorCustomHttpsConfiguration_CustomHttps(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_frontdoor_custom_https_configuration", "test") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMFrontDoorDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMFrontDoorCustomHttpsConfiguration_CustomHttpsEnabled(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFrontDoorCustomHttpsConfigurationExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "custom_https_provisioning_enabled", "true"), + resource.TestCheckResourceAttr(data.ResourceName, "custom_https_configuration.0.certificate_source", "FrontDoor"), + ), + }, + { + Config: testAccAzureRMFrontDoorCustomHttpsConfiguration_CustomHttpsDisabled(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMFrontDoorCustomHttpsConfigurationExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "custom_https_provisioning_enabled", "false"), + ), + }, + }, + }) +} + +func testCheckAzureRMFrontDoorCustomHttpsConfigurationExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).Frontdoor.FrontDoorsFrontendClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Front Door Custom Https Configuration not found: %s", resourceName) + } + + resourceGroup := rs.Primary.Attributes["resource_group_name"] + id, err := azure.ParseAzureResourceID(rs.Primary.Attributes["frontend_endpoint_id"]) + if err != nil { + return fmt.Errorf("Bad: cannot parse frontend_endpoint_id for %q", resourceName) + } + frontDoorName := id.Path["frontdoors"] + // Link to issue: https://github.com/Azure/azure-sdk-for-go/issues/6762 + if frontDoorName == "" { + frontDoorName = id.Path["Frontdoors"] + } + frontendEndpointName := id.Path["frontendendpoints"] + if frontendEndpointName == "" { + frontDoorName = id.Path["FrontendEndpoints"] + } + + resp, err := client.Get(ctx, resourceGroup, frontDoorName, frontendEndpointName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Front Door (%q) Frontend Endpoint %q (Resource Group %q) does not exist", frontDoorName, frontendEndpointName, resourceGroup) + } + return fmt.Errorf("Bad: Get on FrontDoorsFrontendClient: %+v", err) + } + + return nil + } +} + +func testAccAzureRMFrontDoorCustomHttpsConfiguration_CustomHttpsEnabled(data acceptance.TestData) string { + template := testAccAzureRMFrontDoorCustomHttpsConfiguration_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_frontdoor_custom_https_configuration" "test" { + frontend_endpoint_id = azurerm_frontdoor.test.frontend_endpoint[0].id + resource_group_name = azurerm_resource_group.test.name + custom_https_provisioning_enabled = true + + custom_https_configuration { + certificate_source = "FrontDoor" + } +} +`, template) +} + +func testAccAzureRMFrontDoorCustomHttpsConfiguration_CustomHttpsDisabled(data acceptance.TestData) string { + template := testAccAzureRMFrontDoorCustomHttpsConfiguration_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_frontdoor_custom_https_configuration" "test" { + frontend_endpoint_id = azurerm_frontdoor.test.frontend_endpoint[0].id + resource_group_name = azurerm_resource_group.test.name + custom_https_provisioning_enabled = false +} +`, template) +} + +func testAccAzureRMFrontDoorCustomHttpsConfiguration_template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-frontdoor-%d" + location = "%s" +} + +locals { + backend_name = "backend-bing-custom" + endpoint_name = "frontend-endpoint-custom" + health_probe_name = "health-probe-custom" + load_balancing_name = "load-balancing-setting-custom" +} + +resource "azurerm_frontdoor" "test" { + name = "acctest-FD-%d" + resource_group_name = azurerm_resource_group.test.name + enforce_backend_pools_certificate_name_check = false + + routing_rule { + name = "routing-rule" + accepted_protocols = ["Http", "Https"] + patterns_to_match = ["/*"] + frontend_endpoints = [local.endpoint_name] + + forwarding_configuration { + forwarding_protocol = "MatchRequest" + backend_pool_name = local.backend_name + } + } + + backend_pool_load_balancing { + name = local.load_balancing_name + } + + backend_pool_health_probe { + name = local.health_probe_name + } + + backend_pool { + name = local.backend_name + backend { + host_header = "www.bing.com" + address = "www.bing.com" + http_port = 80 + https_port = 443 + } + + load_balancing_name = local.load_balancing_name + health_probe_name = local.health_probe_name + } + + frontend_endpoint { + name = local.endpoint_name + host_name = "acctest-FD-%d.azurefd.net" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) +} diff --git a/azurerm/internal/services/frontdoor/validate/validate.go b/azurerm/internal/services/frontdoor/validate/validate.go index 0b04db956a21..b4aedd8017db 100644 --- a/azurerm/internal/services/frontdoor/validate/validate.go +++ b/azurerm/internal/services/frontdoor/validate/validate.go @@ -92,9 +92,25 @@ func FrontdoorSettings(d *schema.ResourceDiff) error { return fmt.Errorf(`%+v`, err) } - // Verify frontend endpoints custom https configuration is valid if defined - if err := verifyCustomHttpsConfiguration(configFrontendEndpoints); err != nil { - return fmt.Errorf(`%+v`, err) + return nil +} + +func FrontdoorCustomHttpsSettings(d *schema.ResourceDiff) error { + frontendId := d.Get("frontend_endpoint_id").(string) + frontendEndpointCustomHttpsConfig := d.Get("custom_https_configuration").([]interface{}) + customHttpsEnabled := d.Get("custom_https_provisioning_enabled").(bool) + + if len(frontendEndpointCustomHttpsConfig) > 0 { + if !customHttpsEnabled { + return fmt.Errorf(`"frontend_endpoint":%q "custom_https_configuration" is invalid because "custom_https_provisioning_enabled" is set to "false". please remove the "custom_https_configuration" block from the configuration file`, frontendId) + } + + // Verify frontend endpoints custom https configuration is valid if defined + if err := verifyCustomHttpsConfiguration(frontendEndpointCustomHttpsConfig, frontendId); err != nil { + return fmt.Errorf(`%+v`, err) + } + } else if customHttpsEnabled { + return fmt.Errorf(`"frontend_endpoint":%q "custom_https_configuration" is invalid because "custom_https_provisioning_enabled" is set to "true". please add a "custom_https_configuration" block to the configuration file`, frontendId) } return nil @@ -188,29 +204,16 @@ func verifyLoadBalancingAndHealthProbeSettings(backendPools []interface{}, loadB return nil } -func verifyCustomHttpsConfiguration(configFrontendEndpoints []interface{}) error { - for _, configFrontendEndpoint := range configFrontendEndpoints { - if configFrontend := configFrontendEndpoint.(map[string]interface{}); len(configFrontend) > 0 { - FrontendName := configFrontend["name"] - customHttpsEnabled := configFrontend["custom_https_provisioning_enabled"].(bool) - - if chc := configFrontend["custom_https_configuration"].([]interface{}); len(chc) > 0 { - if !customHttpsEnabled { - return fmt.Errorf(`"frontend_endpoint":%q "custom_https_configuration" is invalid because "custom_https_provisioning_enabled" is set to "false". please remove the "custom_https_configuration" block from the configuration file`, FrontendName) - } - - customHttpsConfiguration := chc[0].(map[string]interface{}) - certificateSource := customHttpsConfiguration["certificate_source"] - if certificateSource == string(frontdoor.CertificateSourceAzureKeyVault) { - if !azureKeyVaultCertificateHasValues(customHttpsConfiguration, true) { - return fmt.Errorf(`"frontend_endpoint":%q "custom_https_configuration" is invalid, all of the following keys must have values in the "custom_https_configuration" block: "azure_key_vault_certificate_secret_name", "azure_key_vault_certificate_secret_version", and "azure_key_vault_certificate_vault_id"`, FrontendName) - } - } else if azureKeyVaultCertificateHasValues(customHttpsConfiguration, false) { - return fmt.Errorf(`"frontend_endpoint":%q "custom_https_configuration" is invalid, all of the following keys must be removed from the "custom_https_configuration" block: "azure_key_vault_certificate_secret_name", "azure_key_vault_certificate_secret_version", and "azure_key_vault_certificate_vault_id"`, FrontendName) - } - } else if customHttpsEnabled { - return fmt.Errorf(`"frontend_endpoint":%q configuration is invalid because "custom_https_provisioning_enabled" is set to "true" and the "custom_https_configuration" block is undefined. please add the "custom_https_configuration" block to the configuration file`, FrontendName) +func verifyCustomHttpsConfiguration(frontendEndpointCustomHttpsConfig []interface{}, frontendId string) error { + if len(frontendEndpointCustomHttpsConfig) > 0 { + customHttpsConfiguration := frontendEndpointCustomHttpsConfig[0].(map[string]interface{}) + certificateSource := customHttpsConfiguration["certificate_source"] + if certificateSource == string(frontdoor.CertificateSourceAzureKeyVault) { + if !azureKeyVaultCertificateHasValues(customHttpsConfiguration, true) { + return fmt.Errorf(`"frontend_endpoint":%q "custom_https_configuration" is invalid, all of the following keys must have values in the "custom_https_configuration" block: "azure_key_vault_certificate_secret_name", "azure_key_vault_certificate_secret_version", and "azure_key_vault_certificate_vault_id"`, frontendId) } + } else if azureKeyVaultCertificateHasValues(customHttpsConfiguration, false) { + return fmt.Errorf(`"frontend_endpoint":%q "custom_https_configuration" is invalid, all of the following keys must be removed from the "custom_https_configuration" block: "azure_key_vault_certificate_secret_name", "azure_key_vault_certificate_secret_version", and "azure_key_vault_certificate_vault_id"`, frontendId) } } diff --git a/website/azurerm.erb b/website/azurerm.erb index fc74190cc8f1..d24fe2035094 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -1688,6 +1688,9 @@