diff --git a/internal/services/cdn/cdn_endpoint_custom_domain_resource.go b/internal/services/cdn/cdn_endpoint_custom_domain_resource.go index 58a5a87119bc..3dc453544d50 100644 --- a/internal/services/cdn/cdn_endpoint_custom_domain_resource.go +++ b/internal/services/cdn/cdn_endpoint_custom_domain_resource.go @@ -9,6 +9,7 @@ import ( "github.com/Azure/azure-sdk-for-go/services/cdn/mgmt/2020-09-01/cdn" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/services/cdn/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/services/cdn/validate" keyvaultClient "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/client" @@ -21,6 +22,112 @@ import ( ) func resourceArmCdnEndpointCustomDomain() *pluginsdk.Resource { + schema := map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.CdnEndpointCustomDomainName(), + }, + + "cdn_endpoint_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.EndpointID, + }, + + "host_name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "cdn_managed_https": { + Type: pluginsdk.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "certificate_type": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(cdn.CertificateTypeShared), + string(cdn.CertificateTypeDedicated), + }, false), + }, + "protocol_type": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(cdn.ProtocolTypeServerNameIndication), + string(cdn.ProtocolTypeIPBased), + }, false), + }, + "tls_version": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(cdn.MinimumTLSVersionTLS10), + string(cdn.MinimumTLSVersionTLS12), + string(cdn.MinimumTLSVersionNone), + }, false), + Default: string(cdn.MinimumTLSVersionTLS12), + }, + }, + }, + ConflictsWith: []string{"user_managed_https"}, + }, + + "user_managed_https": { + Type: pluginsdk.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "tls_version": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(cdn.MinimumTLSVersionTLS10), + string(cdn.MinimumTLSVersionTLS12), + string(cdn.MinimumTLSVersionNone), + }, false), + Default: string(cdn.MinimumTLSVersionTLS12), + }, + }, + }, + ConflictsWith: []string{"cdn_managed_https"}, + }, + } + if !features.FourPointOhBeta() { + schema["user_managed_https"].Elem.(*pluginsdk.Resource).Schema["key_vault_certificate_id"] = &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ValidateFunc: keyvaultValidate.NestedItemIdWithOptionalVersion, + ExactlyOneOf: []string{"user_managed_https.0.key_vault_certificate_id", "user_managed_https.0.key_vault_secret_id"}, + Deprecated: "This is deprecated in favor of `key_vault_secret_id` as the service is actually looking for a secret, not a certificate", + } + + schema["user_managed_https"].Elem.(*pluginsdk.Resource).Schema["key_vault_secret_id"] = &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ValidateFunc: keyvaultValidate.NestedItemIdWithOptionalVersion, + ExactlyOneOf: []string{"user_managed_https.0.key_vault_certificate_id", "user_managed_https.0.key_vault_secret_id"}, + } + } else { + schema["user_managed_https"].Elem.(*pluginsdk.Resource).Schema["key_vault_secret_id"] = &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: keyvaultValidate.NestedItemIdWithOptionalVersion, + } + } + return &pluginsdk.Resource{ Create: resourceArmCdnEndpointCustomDomainCreate, Read: resourceArmCdnEndpointCustomDomainRead, @@ -39,92 +146,7 @@ func resourceArmCdnEndpointCustomDomain() *pluginsdk.Resource { Delete: pluginsdk.DefaultTimeout(12 * time.Hour), }, - Schema: map[string]*pluginsdk.Schema{ - "name": { - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validate.CdnEndpointCustomDomainName(), - }, - - "cdn_endpoint_id": { - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validate.EndpointID, - }, - - "host_name": { - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.StringIsNotEmpty, - }, - "cdn_managed_https": { - Type: pluginsdk.TypeList, - Optional: true, - MinItems: 1, - MaxItems: 1, - Elem: &pluginsdk.Resource{ - Schema: map[string]*pluginsdk.Schema{ - "certificate_type": { - Type: pluginsdk.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice([]string{ - string(cdn.CertificateTypeShared), - string(cdn.CertificateTypeDedicated), - }, false), - }, - "protocol_type": { - Type: pluginsdk.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice([]string{ - string(cdn.ProtocolTypeServerNameIndication), - string(cdn.ProtocolTypeIPBased), - }, false), - }, - "tls_version": { - Type: pluginsdk.TypeString, - Optional: true, - ValidateFunc: validation.StringInSlice([]string{ - string(cdn.MinimumTLSVersionTLS10), - string(cdn.MinimumTLSVersionTLS12), - string(cdn.MinimumTLSVersionNone), - }, false), - Default: string(cdn.MinimumTLSVersionTLS12), - }, - }, - }, - ConflictsWith: []string{"user_managed_https"}, - }, - - "user_managed_https": { - Type: pluginsdk.TypeList, - Optional: true, - MinItems: 1, - MaxItems: 1, - Elem: &pluginsdk.Resource{ - Schema: map[string]*pluginsdk.Schema{ - "key_vault_certificate_id": { - Type: pluginsdk.TypeString, - Required: true, - ValidateFunc: keyvaultValidate.NestedItemIdWithOptionalVersion, - }, - "tls_version": { - Type: pluginsdk.TypeString, - Optional: true, - ValidateFunc: validation.StringInSlice([]string{ - string(cdn.MinimumTLSVersionTLS10), - string(cdn.MinimumTLSVersionTLS12), - string(cdn.MinimumTLSVersionNone), - }, false), - Default: string(cdn.MinimumTLSVersionTLS12), - }, - }, - }, - ConflictsWith: []string{"cdn_managed_https"}, - }, - }, + Schema: schema, } } @@ -351,12 +373,21 @@ func resourceArmCdnEndpointCustomDomainRead(d *pluginsdk.ResourceData, meta inte case cdn.UserManagedHTTPSParameters: var isVersioned bool if b := d.Get("user_managed_https").([]interface{}); len(b) == 1 { - if certIdRaw := b[0].(map[string]interface{})["key_vault_certificate_id"].(string); certIdRaw != "" { - certId, err := keyvaultParse.ParseOptionallyVersionedNestedItemID(certIdRaw) + b := b[0].(map[string]interface{}) + + secretIdRaw := b["key_vault_secret_id"].(string) + if !features.FourPointOhBeta() { + if secretIdRaw == "" { + secretIdRaw = b["key_vault_certificate_id"].(string) + } + } + + if secretIdRaw != "" { + id, err := keyvaultParse.ParseOptionallyVersionedNestedItemID(secretIdRaw) if err != nil { - return fmt.Errorf("parsing Key Vault Certificate Id %q: %v", certIdRaw, err) + return fmt.Errorf("parsing Key Vault Secret Id %q: %v", secretIdRaw, err) } - isVersioned = certId.Version != "" + isVersioned = id.Version != "" } } settings, err := flattenArmCdnEndpointCustomDomainUserManagedHttpsSettings(ctx, params, keyVaultsClient, isVersioned) @@ -422,17 +453,24 @@ func expandArmCdnEndpointCustomDomainUserManagedHttpsSettings(ctx context.Contex raw := input[0].(map[string]interface{}) - keyVaultCertId, err := keyvaultParse.ParseOptionallyVersionedNestedItemID(raw["key_vault_certificate_id"].(string)) + idLiteral := raw["key_vault_secret_id"].(string) + if !features.FourPointOhBeta() { + if idLiteral == "" { + idLiteral = raw["key_vault_certificate_id"].(string) + } + } + + keyVaultSecretId, err := keyvaultParse.ParseOptionallyVersionedNestedItemID(idLiteral) if err != nil { return nil, err } - keyVaultIdRaw, err := clients.KeyVault.KeyVaultIDFromBaseUrl(ctx, clients.Resource, keyVaultCertId.KeyVaultBaseUrl) + keyVaultIdRaw, err := clients.KeyVault.KeyVaultIDFromBaseUrl(ctx, clients.Resource, keyVaultSecretId.KeyVaultBaseUrl) if err != nil { - return nil, fmt.Errorf("retrieving the Resource ID the Key Vault at URL %q: %s", keyVaultCertId.KeyVaultBaseUrl, err) + return nil, fmt.Errorf("retrieving the Resource ID the Key Vault at URL %q: %s", keyVaultSecretId.KeyVaultBaseUrl, err) } if keyVaultIdRaw == nil { - return nil, fmt.Errorf("unexpected nil Key Vault ID retrieved at URL %q", keyVaultCertId.KeyVaultBaseUrl) + return nil, fmt.Errorf("unexpected nil Key Vault ID retrieved at URL %q", keyVaultSecretId.KeyVaultBaseUrl) } keyVaultId, err := keyvaultParse.VaultID(*keyVaultIdRaw) if err != nil { @@ -445,8 +483,8 @@ func expandArmCdnEndpointCustomDomainUserManagedHttpsSettings(ctx context.Contex SubscriptionID: &keyVaultId.SubscriptionId, ResourceGroupName: &keyVaultId.ResourceGroup, VaultName: &keyVaultId.Name, - SecretName: &keyVaultCertId.Name, - SecretVersion: &keyVaultCertId.Version, + SecretName: &keyVaultSecretId.Name, + SecretVersion: &keyVaultSecretId.Version, UpdateRule: utils.String("NoAction"), DeleteRule: utils.String("NoAction"), }, @@ -507,31 +545,50 @@ func flattenArmCdnEndpointCustomDomainUserManagedHttpsSettings(ctx context.Conte keyVaultId := keyvaultParse.NewVaultID(subscriptionId, resourceGroupName, vaultName) keyVaultBaseUrl, err := keyVaultsClient.BaseUriForKeyVault(ctx, keyVaultId) if err != nil { - return nil, fmt.Errorf("looking up Key Vault Certificate %q vault url from id %q: %+v", vaultName, keyVaultId, err) + return nil, fmt.Errorf("looking up Key Vault Secret %q vault url from id %q: %+v", vaultName, keyVaultId, err) } - cert, err := keyVaultsClient.ManagementClient.GetCertificate(ctx, *keyVaultBaseUrl, secretName, secretVersion) + + secret, err := keyVaultsClient.ManagementClient.GetSecret(ctx, *keyVaultBaseUrl, secretName, secretVersion) if err != nil { return nil, err } - if cert.ID == nil { - return nil, fmt.Errorf("unexpected null Key Vault Certificate retrieved for Key Vault %s / Secret Name %s / Secret Version %s", keyVaultId, secretName, secretVersion) + if secret.ID == nil { + return nil, fmt.Errorf("unexpected null Key Vault Secret retrieved for Key Vault %s / Secret Name %s / Secret Version %s", keyVaultId, secretName, secretVersion) } - certId, err := keyvaultParse.ParseOptionallyVersionedNestedItemID(*cert.ID) + secretId, err := keyvaultParse.ParseOptionallyVersionedNestedItemID(*secret.ID) if err != nil { return nil, err } - certIdLiteral := certId.ID() + secretIdLiteral := secretId.ID() if !isVersioned { - certIdLiteral = certId.VersionlessID() + secretIdLiteral = secretId.VersionlessID() } - return []interface{}{ - map[string]interface{}{ - "key_vault_certificate_id": certIdLiteral, - "tls_version": string(input.MinimumTLSVersion), - }, - }, nil + m := map[string]interface{}{ + "key_vault_secret_id": secretIdLiteral, + "tls_version": string(input.MinimumTLSVersion), + } + + if features.FourPointOhBeta() { + return []interface{}{m}, nil + } + + // We try to retrieve the certificate with the given secret name and version. If it returns error, then we tolerate the error and simply setting empty string for the certificate id. + // As in this case, the users might be using a secret rather than a certificate. + var certIdLiteral string + cert, err := keyVaultsClient.ManagementClient.GetCertificate(ctx, *keyVaultBaseUrl, secretName, secretVersion) + if err == nil && cert.ID != nil { + certId, err := keyvaultParse.ParseOptionallyVersionedNestedItemID(*cert.ID) + if err == nil { + } + certIdLiteral = certId.ID() + if !isVersioned { + certIdLiteral = certId.VersionlessID() + } + } + m["key_vault_certificate_id"] = certIdLiteral + return []interface{}{m}, nil } func enableArmCdnEndpointCustomDomainHttps(ctx context.Context, client *cdn.CustomDomainsClient, id parse.CustomDomainId, params cdn.BasicCustomDomainHTTPSParameters) error { diff --git a/internal/services/cdn/cdn_endpoint_custom_domain_resource_test.go b/internal/services/cdn/cdn_endpoint_custom_domain_resource_test.go index 67d4e9e17fdd..cd85001600ef 100644 --- a/internal/services/cdn/cdn_endpoint_custom_domain_resource_test.go +++ b/internal/services/cdn/cdn_endpoint_custom_domain_resource_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/services/cdn/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/utils" @@ -85,7 +86,7 @@ func TestAccCdnEndpointCustomDomain_httpsCdn(t *testing.T) { }) } -func TestAccCdnEndpointCustomDomain_httpsUserManaged(t *testing.T) { +func TestAccCdnEndpointCustomDomain_httpsUserManagedCertificate(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_cdn_endpoint_custom_domain", "test") r := NewCdnEndpointCustomDomainResource(os.Getenv("ARM_TEST_DNS_ZONE_RESOURCE_GROUP_NAME"), os.Getenv("ARM_TEST_DNS_ZONE_NAME")) @@ -95,15 +96,56 @@ func TestAccCdnEndpointCustomDomain_httpsUserManaged(t *testing.T) { data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.httpsUserManaged(data), + Config: r.httpsUserManagedCertificate(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, - // The "key_vault_certificate_id" is skipped here since during import, there is no knowledge about whether users want - // versioned or versionless certificate id. That means the imported "key_vault_certificate_id" is what it is at the + // The "key_vault_secret_id" is skipped here since during import, there is no knowledge about whether users want + // versioned or versionless certificate id. That means the imported "key_vault_secret_id" is what it is at the // remote API representation, which might be different than it as defined in the configuration. - data.ImportStep("user_managed_https.0.key_vault_certificate_id"), + data.ImportStep("user_managed_https.0.key_vault_secret_id", "user_managed_https.0.key_vault_certificate_id"), + }) +} + +func TestAccCdnEndpointCustomDomain_httpsUserManagedCertificateDeprecated(t *testing.T) { + if features.FourPointOhBeta() { + t.Skipf("This test is skipped since v4.0") + } + data := acceptance.BuildTestData(t, "azurerm_cdn_endpoint_custom_domain", "test") + + r := NewCdnEndpointCustomDomainResource(os.Getenv("ARM_TEST_DNS_ZONE_RESOURCE_GROUP_NAME"), os.Getenv("ARM_TEST_DNS_ZONE_NAME")) + r.CertificateP12 = os.Getenv("ARM_TEST_DNS_CERTIFICATE") + r.SubDomainName = os.Getenv("ARM_TEST_DNS_SUBDOMAIN_NAME") + r.preCheckUserManagedCertificate(t) + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.httpsUserManagedCertificateDeprecated(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("user_managed_https.0.key_vault_secret_id", "user_managed_https.0.key_vault_certificate_id"), + }) +} + +func TestAccCdnEndpointCustomDomain_httpsUserManagedSecret(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_cdn_endpoint_custom_domain", "test") + + r := NewCdnEndpointCustomDomainResource(os.Getenv("ARM_TEST_DNS_ZONE_RESOURCE_GROUP_NAME"), os.Getenv("ARM_TEST_DNS_ZONE_NAME")) + r.CertificateP12 = os.Getenv("ARM_TEST_DNS_CERTIFICATE") + r.SubDomainName = os.Getenv("ARM_TEST_DNS_SUBDOMAIN_NAME") + r.preCheckUserManagedCertificate(t) + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.httpsUserManagedSecret(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("user_managed_https.0.key_vault_secret_id", "user_managed_https.0.key_vault_certificate_id"), }) } @@ -131,12 +173,12 @@ func TestAccCdnEndpointCustomDomain_httpsUpdate(t *testing.T) { }, data.ImportStep(), { - Config: r.httpsUserManaged(data), + Config: r.httpsUserManagedCertificate(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, - data.ImportStep("user_managed_https.0.key_vault_certificate_id"), + data.ImportStep("user_managed_https.0.key_vault_secret_id", "user_managed_https.0.key_vault_certificate_id"), { Config: r.basic(data), Check: acceptance.ComposeTestCheckFunc( @@ -241,7 +283,7 @@ resource "azurerm_cdn_endpoint_custom_domain" "test" { `, template, data.RandomIntOfLength(8)) } -func (r CdnEndpointCustomDomainResource) httpsUserManaged(data acceptance.TestData) string { +func (r CdnEndpointCustomDomainResource) httpsUserManagedBase(data acceptance.TestData) string { template := r.template(data) return fmt.Sprintf(` %[1]s @@ -275,6 +317,8 @@ resource "azurerm_key_vault" "test" { secret_permissions = [ "Get", "Set", + "Delete", + "Purge", ] } access_policy { @@ -290,6 +334,52 @@ resource "azurerm_key_vault" "test" { ] } } +`, template, data.RandomIntOfLength(8)) +} + +func (r CdnEndpointCustomDomainResource) httpsUserManagedCertificate(data acceptance.TestData) string { + template := r.httpsUserManagedBase(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_key_vault_certificate" "test" { + name = "testkeyvaultcert-%[2]d" + key_vault_id = azurerm_key_vault.test.id + certificate { + contents = file("%[3]s") + password = "" + } + certificate_policy { + issuer_parameters { + name = "Self" + } + key_properties { + exportable = true + key_size = 2048 + key_type = "RSA" + reuse_key = false + } + secret_properties { + content_type = "application/x-pkcs12" + } + } +} +resource "azurerm_cdn_endpoint_custom_domain" "test" { + name = "testcustomdomain-%[2]d" + cdn_endpoint_id = azurerm_cdn_endpoint.test.id + host_name = "${azurerm_dns_cname_record.test.name}.${data.azurerm_dns_zone.test.name}" + user_managed_https { + key_vault_secret_id = azurerm_key_vault_certificate.test.secret_id + } +} +`, template, data.RandomIntOfLength(8), r.CertificateP12) +} + +func (r CdnEndpointCustomDomainResource) httpsUserManagedCertificateDeprecated(data acceptance.TestData) string { + template := r.httpsUserManagedBase(data) + return fmt.Sprintf(` +%[1]s + resource "azurerm_key_vault_certificate" "test" { name = "testkeyvaultcert-%[2]d" key_vault_id = azurerm_key_vault.test.id @@ -323,6 +413,28 @@ resource "azurerm_cdn_endpoint_custom_domain" "test" { `, template, data.RandomIntOfLength(8), r.CertificateP12) } +func (r CdnEndpointCustomDomainResource) httpsUserManagedSecret(data acceptance.TestData) string { + template := r.httpsUserManagedBase(data) + return fmt.Sprintf(` +%[1]s + +resource "azurerm_key_vault_secret" "test" { + name = "testkeyvaultsecret-%[2]d" + key_vault_id = azurerm_key_vault.test.id + content_type = "application/x-pkcs12" + value = file("%[3]s") +} +resource "azurerm_cdn_endpoint_custom_domain" "test" { + name = "testcustomdomain-%[2]d" + cdn_endpoint_id = azurerm_cdn_endpoint.test.id + host_name = "${azurerm_dns_cname_record.test.name}.${data.azurerm_dns_zone.test.name}" + user_managed_https { + key_vault_secret_id = azurerm_key_vault_secret.test.id + } +} +`, template, data.RandomIntOfLength(8), r.CertificateP12) +} + func (r CdnEndpointCustomDomainResource) template(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { diff --git a/website/docs/r/cdn_endpoint_custom_domain.html.markdown b/website/docs/r/cdn_endpoint_custom_domain.html.markdown index 2fac5ed85909..f99e6a8fde73 100644 --- a/website/docs/r/cdn_endpoint_custom_domain.html.markdown +++ b/website/docs/r/cdn_endpoint_custom_domain.html.markdown @@ -95,7 +95,11 @@ A `cdn_managed_https` block supports the following: A `user_managed_https` block supports the following: -* `key_vault_certificate_id` - (Required) The ID of the Key Vault Certificate that contains the HTTPS certificate. +* `key_vault_certificate_id` - (Optional) The ID of the Key Vault Certificate that contains the HTTPS certificate. This is deprecated in favor of `key_vault_secret_id`. + +* `key_vault_secret_id` - (Optional) The ID of the Key Vault Secret that contains the HTTPS certificate. + +~> **NOTE** Either `key_vault_certificate_id` or `key_vault_secret_id` has to be specified. * `tls_version` - (Optional) The minimum TLS protocol version that is used for HTTPS. Possible values are `TLS10` (representing TLS 1.0/1.1), `TLS12` (representing TLS 1.2) and `None` (representing no minimums). Defaults to `TLS12`.