Skip to content

Commit

Permalink
Merge pull request #778 from jacobbednarz/certificate-pack-support
Browse files Browse the repository at this point in the history
Add support for TLS certificate packs
  • Loading branch information
jacobbednarz authored Sep 2, 2020
2 parents 81d348c + de67005 commit 2d18860
Show file tree
Hide file tree
Showing 5 changed files with 380 additions and 0 deletions.
1 change: 1 addition & 0 deletions cloudflare/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func Provider() terraform.ResourceProvider {
"cloudflare_authenticated_origin_pulls": resourceCloudflareAuthenticatedOriginPulls(),
"cloudflare_authenticated_origin_pulls_certificate": resourceCloudflareAuthenticatedOriginPullsCertificate(),
"cloudflare_byo_ip_prefix": resourceCloudflareBYOIPPrefix(),
"cloudflare_certificate_pack": resourceCloudflareCertificatePack(),
"cloudflare_custom_hostname": resourceCloudflareCustomHostname(),
"cloudflare_custom_hostname_fallback_origin": resourceCloudflareCustomHostnameFallbackOrigin(),
"cloudflare_custom_pages": resourceCloudflareCustomPages(),
Expand Down
161 changes: 161 additions & 0 deletions cloudflare/resource_cloudflare_certificate_pack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package cloudflare

import (
"fmt"
"log"
"strings"

cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
"github.com/pkg/errors"
)

func resourceCloudflareCertificatePack() *schema.Resource {
return &schema.Resource{
// Intentionally no Update method as certificates require replacement for
// any changes made.
Create: resourceCloudflareCertificatePackCreate,
Read: resourceCloudflareCertificatePackRead,
Delete: resourceCloudflareCertificatePackDelete,
Importer: &schema.ResourceImporter{
State: resourceCloudflareCertificatePackImport,
},

Schema: map[string]*schema.Schema{
"zone_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"type": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{"custom", "dedicated_custom", "advanced"}, false),
},
"hosts": {
Type: schema.TypeList,
Required: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"validation_method": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{"txt", "http", "email"}, false),
},
"validity_days": {
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
ValidateFunc: validation.IntInSlice([]int{14, 30, 90, 365}),
},
"certificate_authority": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{"digicert", "lets_encrypt"}, false),
},
"cloudflare_branding": {
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
},
}
}

func resourceCloudflareCertificatePackCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
zoneID := d.Get("zone_id").(string)
certificatePackType := d.Get("type").(string)
certificateHostnames := expandInterfaceToStringList(d.Get("hosts").([]interface{}))
certificatePackID := ""

if certificatePackType == "advanced" {
validationMethod := d.Get("validation_method").(string)
validityDays := d.Get("validity_days").(int)
ca := d.Get("certificate_authority").(string)
cloudflareBranding := d.Get("cloudflare_branding").(bool)

cert := cloudflare.CertificatePackAdvancedCertificate{
Type: "advanced",
Hosts: certificateHostnames,
ValidationMethod: validationMethod,
ValidityDays: validityDays,
CertificateAuthority: ca,
CloudflareBranding: cloudflareBranding,
}
certPackResponse, err := client.CreateAdvancedCertificatePack(zoneID, cert)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to create certificate pack: %s", err))
}
certificatePackID = certPackResponse.ID
} else {
cert := cloudflare.CertificatePackRequest{
Type: certificatePackType,
Hosts: certificateHostnames,
}
certPackResponse, err := client.CreateCertificatePack(zoneID, cert)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to create certificate pack: %s", err))
}
certificatePackID = certPackResponse.ID
}

d.SetId(certificatePackID)

return resourceCloudflareCertificatePackRead(d, meta)
}

func resourceCloudflareCertificatePackRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
zoneID := d.Get("zone_id").(string)

certificatePack, err := client.CertificatePack(zoneID, d.Id())
if err != nil {
return errors.Wrap(err, "failed to fetch certificate pack")
}

d.Set("type", certificatePack.Type)
d.Set("hosts", flattenStringList(certificatePack.Hosts))

return nil
}

func resourceCloudflareCertificatePackDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
zoneID := d.Get("zone_id").(string)

err := client.DeleteCertificatePack(zoneID, d.Id())
if err != nil {
return errors.Wrap(err, "failed to delete certificate pack")
}

resourceCloudflareCertificatePackRead(d, meta)

return nil
}

func resourceCloudflareCertificatePackImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
attributes := strings.SplitN(d.Id(), "/", 2)

if len(attributes) != 2 {
return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"zoneID/certificatePackID\"", d.Id())
}

zoneID, certificatePackID := attributes[0], attributes[1]

log.Printf("[DEBUG] Importing Cloudflare Certificate Pack: id %s for zone %s", certificatePackID, zoneID)

d.Set("zone_id", zoneID)
d.SetId(certificatePackID)

resourceCloudflareCertificatePackRead(d, meta)

return []*schema.ResourceData{d}, nil
}
130 changes: 130 additions & 0 deletions cloudflare/resource_cloudflare_certificate_pack_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package cloudflare

import (
"fmt"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
)

func TestAccCertificatePackAdvancedDigicert(t *testing.T) {
rnd := generateRandomResourceName()
name := "cloudflare_certificate_pack." + rnd
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
domain := os.Getenv("CLOUDFLARE_DOMAIN")

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCertificatePackAdvancedDigicertConfig(zoneID, domain, "advanced", rnd),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "zone_id", zoneID),
resource.TestCheckResourceAttr(name, "type", "advanced"),
resource.TestCheckResourceAttr(name, "hosts.0", fmt.Sprintf("%s.%s", rnd, domain)),
resource.TestCheckResourceAttr(name, "hosts.1", domain),
resource.TestCheckResourceAttr(name, "validation_method", "http"),
resource.TestCheckResourceAttr(name, "validity_days", "365"),
resource.TestCheckResourceAttr(name, "certificate_authority", "digicert"),
resource.TestCheckResourceAttr(name, "cloudflare_branding", "false"),
),
},
},
})
}

func testAccCertificatePackAdvancedDigicertConfig(zoneID, domain, certType, rnd string) string {
return fmt.Sprintf(`
resource "cloudflare_certificate_pack" "%[3]s" {
zone_id = "%[1]s"
type = "%[4]s"
hosts = [
"%[3]s.%[2]s",
"%[2]s"
]
validation_method = "http"
validity_days = 365
certificate_authority = "digicert"
cloudflare_branding = false
}`, zoneID, domain, rnd, certType)
}

func TestAccCertificatePackAdvancedLetsEncrypt(t *testing.T) {
rnd := generateRandomResourceName()
name := "cloudflare_certificate_pack." + rnd
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
domain := os.Getenv("CLOUDFLARE_DOMAIN")

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCertificatePackAdvancedLetsEncryptConfig(zoneID, domain, "advanced", rnd),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "zone_id", zoneID),
resource.TestCheckResourceAttr(name, "type", "advanced"),
resource.TestCheckResourceAttr(name, "hosts.0", fmt.Sprintf("*.%s", domain)),
resource.TestCheckResourceAttr(name, "hosts.1", domain),
resource.TestCheckResourceAttr(name, "validation_method", "txt"),
resource.TestCheckResourceAttr(name, "validity_days", "90"),
resource.TestCheckResourceAttr(name, "certificate_authority", "lets_encrypt"),
resource.TestCheckResourceAttr(name, "cloudflare_branding", "false"),
),
},
},
})
}

func testAccCertificatePackAdvancedLetsEncryptConfig(zoneID, domain, certType, rnd string) string {
return fmt.Sprintf(`
resource "cloudflare_certificate_pack" "%[3]s" {
zone_id = "%[1]s"
type = "%[4]s"
hosts = [
"*.%[2]s",
"%[2]s"
]
validation_method = "txt"
validity_days = 90
certificate_authority = "lets_encrypt"
cloudflare_branding = false
}`, zoneID, domain, rnd, certType)
}

func TestAccCertificatePackDedicatedCustom(t *testing.T) {
rnd := generateRandomResourceName()
name := "cloudflare_certificate_pack." + rnd
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
domain := os.Getenv("CLOUDFLARE_DOMAIN")

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCertificatePackDedicatedCustomConfig(zoneID, domain, "dedicated_custom", rnd),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "zone_id", zoneID),
resource.TestCheckResourceAttr(name, "type", "dedicated_custom"),
resource.TestCheckResourceAttr(name, "hosts.0", fmt.Sprintf("%s.%s", rnd, domain)),
resource.TestCheckResourceAttr(name, "hosts.1", domain),
),
},
},
})
}

func testAccCertificatePackDedicatedCustomConfig(zoneID, domain, certType, rnd string) string {
return fmt.Sprintf(`
resource "cloudflare_certificate_pack" "%[3]s" {
zone_id = "%[1]s"
type = "%[4]s"
hosts = [
"%[3]s.%[2]s",
"%[2]s"
]
}`, zoneID, domain, rnd, certType)
}
3 changes: 3 additions & 0 deletions website/cloudflare.erb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
<li<%= sidebar_current("docs-cloudflare-resource-byo-ip-prefix") %>>
<a href="/docs/providers/cloudflare/r/byo_ip_prefix.html">cloudflare_byo_ip_prefix</a>
</li>
<li<%= sidebar_current("docs-cloudflare-resource-certificate-pack") %>>
<a href="/docs/providers/cloudflare/r/certificate_pack.html">cloudflare_certificate_pack</a>
</li>
<li<%= sidebar_current("docs-cloudflare-resource-custom-pages") %>>
<a href="/docs/providers/cloudflare/r/custom_pages.html">cloudflare_custom_pages</a>
</li>
Expand Down
85 changes: 85 additions & 0 deletions website/docs/r/certificate_pack.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
layout: "cloudflare"
page_title: "Cloudflare: cloudflare_certificate_pack"
sidebar_current: "docs-cloudflare-resource-certificate-pack"
description: |-
Provides a Cloudflare Certificate Pack resource.
---

# cloudflare_certificate_pack

Provides a Cloudflare Certificate Pack resource that is used to provision
managed TLS certificates.

~> **Important:** Certificate packs are not able to be updated in place and if
you require a zero downtime rotation, you need to use Terraform's meta-arguments
for [`lifecycle`](https://www.terraform.io/docs/configuration/resources.html#lifecycle-lifecycle-customizations) blocks.
`create_before_destroy` should be suffice for most scenarios (exceptions are
things like missing entitlements, high ranking domain). To completely
de-risk rotations, use you can create multiple resources using a 2-phase change
where you have both resources live at once and you remove the old one once
you've confirmed the certificate is available.

## Example Usage

```hcl
resource "cloudflare_certificate_pack" "dedicated_custom_example" {
zone_id = "1d5fdc9e88c8a8c4518b068cd94331fe"
type = "dedicated_custom"
hosts = ["example.com", "sub.example.com"]
}
# Advanced certificate manager for DigiCert
resource "cloudflare_certificate_pack" "advanced_example_for_digicert" {
zone_id = "1d5fdc9e88c8a8c4518b068cd94331fe"
type = "advanced"
hosts = ["example.com", "sub.example.com"]
validation_method = "txt"
validity_days = 30
certificate_authority = "digicert"
cloudflare_branding = false
}
# Advanced certificate manager for Let's Encrypt
resource "cloudflare_certificate_pack" "advanced_example_for_lets_encrypt" {
zone_id = "1d5fdc9e88c8a8c4518b068cd94331fe"
type = "advanced"
hosts = ["example.com", "*.example.com"]
validation_method = "http"
validity_days = 90
certificate_authority = "lets_encrypot"
cloudflare_branding = false
}
```

## Argument Reference

The following arguments are supported:

* `zone_id` - (Required) The DNS zone to which the certificate pack should be added.
* `type` - (Required) Certificate pack configuration type.
Allowed values: `"custom"`, `"dedicated_custom"`, `"advanced"`.
* `hosts` - (Required) List of hostnames to provision the certificate pack for.
Note: If using Let's Encrypt, you cannot use individual subdomains and only a
wildcard for subdomain is available.
* `validation_method` - (Optional based on `type`) Which validation method to
use in order to prove domain ownership. Allowed values: `"txt"`, `"http"`, `"email"`.
* `validity_days` - (Optional based on `type`) How long the certificate is valid
for. Note: If using Let's Encrypt, this value can only be 90 days.
Allowed values: 14, 30, 90, 365.
* `certificate_authority` - (Optional based on `type`) Which certificate
authority to issue the certificate pack. Allowed values: `"digicert"`,
`"lets_encrypt"`.
* `cloudflare_branding` - (Optional based on `type`) Whether or not to include
Cloudflare branding. This will add `sni.cloudflaressl.com` as the Common Name
if set to `true`.

## Import

Certificate packs can be imported using a composite ID of the zone ID and
certificate pack ID. This isn't recommended and it is advised to replace the
certificate entirely instead.

```
$ terraform import cloudflare_certificate_pack.example cb029e245cfdd66dc8d2e570d5dd3322/8fda82e2-6af9-4eb2-992a-5ab65b792ef1
```

0 comments on commit 2d18860

Please sign in to comment.