diff --git a/azurerm/internal/services/web/client.go b/azurerm/internal/services/web/client.go index 064c3f563308..948885766f1f 100644 --- a/azurerm/internal/services/web/client.go +++ b/azurerm/internal/services/web/client.go @@ -8,6 +8,7 @@ import ( type Client struct { AppServicePlansClient *web.AppServicePlansClient AppServicesClient *web.AppsClient + CertificatesClient *web.CertificatesClient } func BuildClient(o *common.ClientOptions) *Client { @@ -18,8 +19,12 @@ func BuildClient(o *common.ClientOptions) *Client { AppServicesClient := web.NewAppsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&AppServicesClient.Client, o.ResourceManagerAuthorizer) + CertificatesClient := web.NewCertificatesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&CertificatesClient.Client, o.ResourceManagerAuthorizer) + return &Client{ AppServicePlansClient: &AppServicePlansClient, AppServicesClient: &AppServicesClient, + CertificatesClient: &CertificatesClient, } } diff --git a/azurerm/provider.go b/azurerm/provider.go index d88ec0a50b23..783beea2a52a 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -144,6 +144,7 @@ func Provider() terraform.ResourceProvider { "azurerm_api_management_subscription": resourceArmApiManagementSubscription(), "azurerm_api_management_user": resourceArmApiManagementUser(), "azurerm_app_service_active_slot": resourceArmAppServiceActiveSlot(), + "azurerm_app_service_certificate": resourceArmAppServiceCertificate(), "azurerm_app_service_custom_hostname_binding": resourceArmAppServiceCustomHostnameBinding(), "azurerm_app_service_plan": resourceArmAppServicePlan(), "azurerm_app_service_slot": resourceArmAppServiceSlot(), diff --git a/azurerm/resource_arm_app_service_certificate.go b/azurerm/resource_arm_app_service_certificate.go new file mode 100644 index 000000000000..320f7e625fa0 --- /dev/null +++ b/azurerm/resource_arm_app_service_certificate.go @@ -0,0 +1,258 @@ +package azurerm + +import ( + "encoding/base64" + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/services/web/mgmt/2018-02-01/web" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/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/tags" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmAppServiceCertificate() *schema.Resource { + return &schema.Resource{ + Create: resourceArmAppServiceCertificateCreateUpdate, + Read: resourceArmAppServiceCertificateRead, + Update: resourceArmAppServiceCertificateCreateUpdate, + Delete: resourceArmAppServiceCertificateDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NoEmptyStrings, + }, + + "location": azure.SchemaLocation(), + + "resource_group_name": azure.SchemaResourceGroupName(), + + "pfx_blob": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ForceNew: true, + ValidateFunc: validate.Base64String(), + }, + + "password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + + "key_vault_secret_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: azure.ValidateKeyVaultChildId, + ConflictsWith: []string{"pfx_blob", "password"}, + }, + + "friendly_name": { + Type: schema.TypeString, + Computed: true, + }, + + "subject_name": { + Type: schema.TypeString, + Computed: true, + }, + + "host_names": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "issuer": { + Type: schema.TypeString, + Computed: true, + }, + + "issue_date": { + Type: schema.TypeString, + Computed: true, + }, + + "expiration_date": { + Type: schema.TypeString, + Computed: true, + }, + + "thumbprint": { + Type: schema.TypeString, + Computed: true, + }, + + "tags": tags.Schema(), + }, + } +} + +func resourceArmAppServiceCertificateCreateUpdate(d *schema.ResourceData, meta interface{}) error { + vaultClient := meta.(*ArmClient).keyvault.VaultsClient + client := meta.(*ArmClient).web.CertificatesClient + ctx := meta.(*ArmClient).StopContext + + log.Printf("[INFO] preparing arguments for App Service Certificate creation.") + + name := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + location := azure.NormalizeLocation(d.Get("location").(string)) + pfxBlob := d.Get("pfx_blob").(string) + password := d.Get("password").(string) + keyVaultSecretId := d.Get("key_vault_secret_id").(string) + t := d.Get("tags").(map[string]interface{}) + + if pfxBlob == "" && keyVaultSecretId == "" { + return fmt.Errorf("Either `pfx_blob` or `key_vault_secret_id` must be set") + } + + if requireResourcesToBeImported && d.IsNewResource() { + existing, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("Error checking for presence of existing App Service Certificate %q (Resource Group %q): %s", name, resourceGroup, err) + } + } + + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_app_service_certificate", *existing.ID) + } + } + + certificate := web.Certificate{ + CertificateProperties: &web.CertificateProperties{ + Password: utils.String(password), + }, + Location: utils.String(location), + Tags: tags.Expand(t), + } + + if pfxBlob != "" { + decodedPfxBlob, err := base64.StdEncoding.DecodeString(pfxBlob) + if err != nil { + return fmt.Errorf("Could not decode PFX blob: %+v", err) + } + certificate.CertificateProperties.PfxBlob = &decodedPfxBlob + } + + if keyVaultSecretId != "" { + parsedSecretId, err := azure.ParseKeyVaultChildID(keyVaultSecretId) + if err != nil { + return err + } + + keyVaultBaseUrl := parsedSecretId.KeyVaultBaseUrl + + keyVaultId, err := azure.GetKeyVaultIDFromBaseUrl(ctx, vaultClient, keyVaultBaseUrl) + if err != nil { + return fmt.Errorf("Error retrieving the Resource ID for the Key Vault at URL %q: %s", keyVaultBaseUrl, err) + } + if keyVaultId == nil { + return fmt.Errorf("Unable to determine the Resource ID for the Key Vault at URL %q", keyVaultBaseUrl) + } + + certificate.CertificateProperties.KeyVaultID = keyVaultId + certificate.CertificateProperties.KeyVaultSecretName = utils.String(parsedSecretId.Name) + } + + if _, err := client.CreateOrUpdate(ctx, resourceGroup, name, certificate); err != nil { + return fmt.Errorf("Error creating/updating App Service Certificate %q (Resource Group %q): %s", name, resourceGroup, err) + } + + read, err := client.Get(ctx, resourceGroup, name) + if err != nil { + return fmt.Errorf("Error retrieving App Service Certificate %q (Resource Group %q): %s", name, resourceGroup, err) + } + if read.ID == nil { + return fmt.Errorf("Cannot read App Service Certificate %q (Resource Group %q) ID", name, resourceGroup) + } + + d.SetId(*read.ID) + + return resourceArmAppServiceCertificateRead(d, meta) +} + +func resourceArmAppServiceCertificateRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).web.CertificatesClient + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resourceGroup := id.ResourceGroup + name := id.Path["certificates"] + + ctx := meta.(*ArmClient).StopContext + resp, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[DEBUG] App Service Certificate %q (Resource Group %q) was not found - removing from state", name, resourceGroup) + d.SetId("") + return nil + } + return fmt.Errorf("Error making Read request on App Service Certificate %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + d.Set("name", resp.Name) + d.Set("resource_group_name", resourceGroup) + + if location := resp.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + + if props := resp.CertificateProperties; props != nil { + d.Set("friendly_name", props.FriendlyName) + d.Set("subject_name", props.SubjectName) + d.Set("host_names", props.HostNames) + d.Set("issuer", props.Issuer) + d.Set("issue_date", props.IssueDate.Format(time.RFC3339)) + d.Set("expiration_date", props.ExpirationDate.Format(time.RFC3339)) + d.Set("thumbprint", props.Thumbprint) + } + + flattenAndSetTags(d, resp.Tags) + + return nil +} + +func resourceArmAppServiceCertificateDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).web.CertificatesClient + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + resourceGroup := id.ResourceGroup + name := id.Path["certificates"] + + log.Printf("[DEBUG] Deleting App Service Certificate %q (Resource Group %q)", name, resourceGroup) + + ctx := meta.(*ArmClient).StopContext + resp, err := client.Delete(ctx, resourceGroup, name) + if err != nil { + if !utils.ResponseWasNotFound(resp) { + return fmt.Errorf("Error deleting App Service Certificate %q (Resource Group %q): %s)", name, resourceGroup, err) + } + } + + return nil +} diff --git a/azurerm/resource_arm_app_service_certificate_test.go b/azurerm/resource_arm_app_service_certificate_test.go new file mode 100644 index 000000000000..d28e419a4bb6 --- /dev/null +++ b/azurerm/resource_arm_app_service_certificate_test.go @@ -0,0 +1,227 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMAppServiceCertificate_Pfx(t *testing.T) { + resourceName := "azurerm_app_service_certificate.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + config := testAccAzureRMAppServiceCertificatePfx(ri, location) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMAppServiceCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "password", "terraform"), + resource.TestCheckResourceAttr(resourceName, "thumbprint", "7B985BF42467791F23E52B364A3E8DEBAB9C606E"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"pfx_blob", "password"}, + }, + }, + }) +} + +func TestAccAzureRMAppServiceCertificate_PfxNoPassword(t *testing.T) { + resourceName := "azurerm_app_service_certificate.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + config := testAccAzureRMAppServiceCertificatePfxNoPassword(ri, location) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMAppServiceCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "thumbprint", "7B985BF42467791F23E52B364A3E8DEBAB9C606E"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"pfx_blob"}, + }, + }, + }) +} + +func TestAccAzureRMAppServiceCertificate_KeyVault(t *testing.T) { + resourceName := "azurerm_app_service_certificate.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + config := testAccAzureRMAppServiceCertificateKeyVault(ri, location) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMAppServiceCertificateDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "thumbprint", "7B985BF42467791F23E52B364A3E8DEBAB9C606E"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"key_vault_secret_id"}, + }, + }, + }) +} + +func testAccAzureRMAppServiceCertificatePfx(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestwebcert%d" + location = "%s" +} + +resource "azurerm_app_service_certificate" "test" { + name = "acctest%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + pfx_blob = filebase64("testdata/app_service_certificate.pfx") + password = "terraform" +} +`, rInt, location, rInt) +} + +func testAccAzureRMAppServiceCertificatePfxNoPassword(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestwebcert%d" + location = "%s" +} + +resource "azurerm_app_service_certificate" "test" { + name = "acctest%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + pfx_blob = filebase64("testdata/app_service_certificate_nopassword.pfx") +} +`, rInt, location, rInt) +} + +func testAccAzureRMAppServiceCertificateKeyVault(rInt int, location string) string { + return fmt.Sprintf(` +data "azurerm_client_config" "test" {} + +data "azuread_service_principal" "test" { + display_name = "Microsoft Azure App Service" +} + +resource "azurerm_resource_group" "test" { + name = "acctestwebcert%d" + location = "%s" +} + +resource "azurerm_key_vault" "test" { + name = "acct%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + tenant_id = data.azurerm_client_config.test.tenant_id + + sku_name = "standard" + + access_policy { + tenant_id = data.azurerm_client_config.test.tenant_id + object_id = data.azurerm_client_config.test.service_principal_object_id + secret_permissions = ["delete", "get", "set"] + certificate_permissions = ["create", "delete", "get", "import"] + } + + access_policy { + tenant_id = data.azurerm_client_config.test.tenant_id + object_id = data.azuread_service_principal.test.object_id + secret_permissions = ["get"] + certificate_permissions = ["get"] + } +} + +resource "azurerm_key_vault_certificate" "test" { + name = "acctest%d" + key_vault_id = azurerm_key_vault.test.id + + certificate { + contents = filebase64("testdata/app_service_certificate.pfx") + password = "terraform" + } + + 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_app_service_certificate" "test" { + name = "acctest%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + key_vault_secret_id = azurerm_key_vault_certificate.test.id +} +`, rInt, location, rInt, rInt, rInt) +} + +func testCheckAzureRMAppServiceCertificateDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*ArmClient).web.CertificatesClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_app_service_certificate" { + continue + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := conn.Get(ctx, resourceGroup, name) + if err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return err + } + } + + return nil + } + + return nil +} diff --git a/azurerm/testdata/app_service_certificate.pfx b/azurerm/testdata/app_service_certificate.pfx new file mode 100644 index 000000000000..ddcfb937654a Binary files /dev/null and b/azurerm/testdata/app_service_certificate.pfx differ diff --git a/azurerm/testdata/app_service_certificate_nopassword.pfx b/azurerm/testdata/app_service_certificate_nopassword.pfx new file mode 100644 index 000000000000..be04af726a1b Binary files /dev/null and b/azurerm/testdata/app_service_certificate_nopassword.pfx differ diff --git a/website/azurerm.erb b/website/azurerm.erb index a41c178da62d..c7781eb28ad2 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -490,6 +490,10 @@ azurerm_app_service_active_slot +