diff --git a/cloudflare/provider.go b/cloudflare/provider.go index 00b5c64b32..4dcf3d4138 100644 --- a/cloudflare/provider.go +++ b/cloudflare/provider.go @@ -95,6 +95,7 @@ func Provider() terraform.ResourceProvider { "cloudflare_account_member": resourceCloudflareAccountMember(), "cloudflare_argo": resourceCloudflareArgo(), "cloudflare_custom_pages": resourceCloudflareCustomPages(), + "cloudflare_custom_ssl": resourceCloudflareCustomSsl(), "cloudflare_filter": resourceCloudflareFilter(), "cloudflare_firewall_rule": resourceCloudflareFirewallRule(), "cloudflare_load_balancer_monitor": resourceCloudflareLoadBalancerMonitor(), diff --git a/cloudflare/resource_cloudflare_custom_ssl.go b/cloudflare/resource_cloudflare_custom_ssl.go new file mode 100644 index 0000000000..215cb9d4ec --- /dev/null +++ b/cloudflare/resource_cloudflare_custom_ssl.go @@ -0,0 +1,361 @@ +package cloudflare + +import ( + "encoding/json" + "fmt" + "log" + "strings" + "time" + + "github.com/hashicorp/terraform/helper/validation" + + cloudflare "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform/helper/schema" + "github.com/pkg/errors" +) + +func resourceCloudflareCustomSsl() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudflareCustomSslCreate, + Read: resourceCloudflareCustomSslRead, + Update: resourceCloudflareCustomSslUpdate, + Delete: resourceCloudflareCustomSslDelete, + Importer: &schema.ResourceImporter{ + State: resourceCloudflareCustomSslImport, + }, + + Schema: map[string]*schema.Schema{ + "zone_id": { + Type: schema.TypeString, + Required: true, + }, + "custom_ssl_priority": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + }, + "priority": { + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, + "custom_ssl_options": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "certificate": { + Type: schema.TypeString, + Optional: false, + }, + "private_key": { + Type: schema.TypeString, + Optional: false, + Sensitive: true, + }, + "bundle_method": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"ubiquitous", "optimal", "force"}, false), + }, + "geo_restrictions": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"us", "eu", "highest_security"}, false), + }, + "type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"legacy_custom", "sni_custom"}, false), + }, + }, + }, + }, + "hosts": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "issuer": { + Type: schema.TypeString, + Computed: true, + }, + "signature": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "uploaded_on": { + Type: schema.TypeString, + Computed: true, + }, + "modified_on": { + Type: schema.TypeString, + Computed: true, + }, + "expires_on": { + Type: schema.TypeString, + Computed: true, + }, + "priority": { + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func resourceCloudflareCustomSslCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + zoneID := d.Get("zone_id").(string) + log.Printf("[DEBUG] zone ID: %s", zoneID) + zcso, err := expandToZoneCustomSSLOptions(d) + if err != nil { + return fmt.Errorf("Failed to create custom ssl cert: %s", err) + } + + res, err := client.CreateSSL(zoneID, zcso) + if err != nil { + return fmt.Errorf("Failed to create custom ssl cert: %s", err) + } + + if res.ID == "" { + return fmt.Errorf("Failed to find custom ssl in Create response: id was empty") + } + + d.SetId(res.ID) + + log.Printf("[INFO] Cloudflare Custom SSL ID: %s", d.Id()) + + return resourceCloudflareCustomSslRead(d, meta) +} + +func resourceCloudflareCustomSslUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + zoneID := d.Get("zone_id").(string) + certID := d.Id() + var uErr error + var reErr error + var updateErr = false + var reprioritizeErr = false + log.Printf("[DEBUG] zone ID: %s", zoneID) + + // Enable partial state mode for atomic subsequent updates + d.Partial(true) + + if d.HasChange("custom_ssl_options") { + zcso, err := expandToZoneCustomSSLOptions(d) + if err != nil { + return fmt.Errorf("Failed to update custom ssl cert: %s", err) + } + + res, uErr := client.UpdateSSL(zoneID, certID, zcso) + if uErr != nil { + log.Printf("[DEBUG] Failed to update custom ssl cert: %s", uErr) + updateErr = true + } else { + d.SetPartial("custom_ssl_options") + log.Printf("[DEBUG] Custom SSL set to: %s", res.ID) + } + + } + + if d.HasChange("custom_ssl_priority") { + zcsp, err := expandToZoneCustomSSLPriority(d) + if err != nil { + log.Printf("Failed to update custom ssl cert: %s", err) + } + + resList, reErr := client.ReprioritizeSSL(zoneID, zcsp) + if err != nil { + log.Printf("Failed to update / reprioritize custom ssl cert: %s", reErr) + reprioritizeErr = true + } else { + d.SetPartial("custom_ssl_priority") + log.Printf("[DEBUG] Custom SSL reprioritized to: %#v", resList) + } + } + + if updateErr && reprioritizeErr { + return fmt.Errorf("Failed to update and reprioritize custom ssl cert: %s, %s", uErr, reErr) + } + // We succeeded so disable partial mode + d.Partial(false) + + return resourceCloudflareCustomSslRead(d, meta) +} + +func resourceCloudflareCustomSslRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + zoneID := d.Get("zone_id").(string) + certID := d.Id() + + // update all possible schema attributes with fields from api response + record, err := client.SSLDetails(zoneID, certID) + if err != nil { + log.Printf("[WARN] Removing record from state because it's not found in API") + d.SetId("") + return nil + } + zcso, err := expandToZoneCustomSSLOptions(d) + if err != nil { + log.Printf("[WARN] Problem setting zone options not read from state %s", err) + } + zcso.BundleMethod = record.BundleMethod + customSslOpts := flattenCustomSSLOptions(zcso) + + // fill in fields that the api doesn't return + data, dataOk := d.GetOk("custom_ssl_options") + newData := make(map[string]string) + if dataOk { + for id, value := range data.(map[string]interface{}) { + newValue := value.(string) + newData[id] = newValue + } + } + if val, ok := newData["%"]; ok { + customSslOpts["%"] = val + } + if val, ok := newData["geo_restrictions"]; ok { + customSslOpts["geo_restrictions"] = val + } + if val, ok := newData["type"]; ok { + customSslOpts["type"] = val + } + + d.SetId(record.ID) + d.Set("hosts", record.Hosts) + d.Set("issuer", record.Issuer) + d.Set("signature", record.Signature) + if err := d.Set("custom_ssl_options", customSslOpts); err != nil { + return fmt.Errorf("[WARN] Error reading custom ssl opts %q: %s", d.Id(), err) + } + d.Set("status", record.Status) + d.Set("uploaded_on", record.UploadedOn.Format(time.RFC3339Nano)) + d.Set("expires_on", record.ExpiresOn.Format(time.RFC3339Nano)) + d.Set("modified_on", record.ModifiedOn.Format(time.RFC3339Nano)) + d.Set("priority", record.Priority) + return nil +} + +func resourceCloudflareCustomSslDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + zoneID := d.Get("zone_id").(string) + certID := d.Id() + + log.Printf("[DEBUG] Deleting SSL cert %s for zone %s", certID, zoneID) + + err := client.DeleteSSL(zoneID, certID) + if err != nil { + errors.Wrap(err, "failed to delete custom ssl cert setting") + } + return nil +} + +func resourceCloudflareCustomSslImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + // split the id so we can lookup + idAttr := strings.SplitN(d.Id(), "/", 2) + if len(idAttr) != 2 { + return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"zoneID/certID\"", d.Id()) + } + + zoneID, certID := idAttr[0], idAttr[1] + + log.Printf("[DEBUG] Importing Cloudflare Custom SSL Cert: id %s for zone %s", certID, zoneID) + + d.Set("zone_id", zoneID) + d.SetId(certID) + + resourceCloudflareCustomSslRead(d, meta) + + return []*schema.ResourceData{d}, nil +} + +func expandToZoneCustomSSLPriority(d *schema.ResourceData) ([]cloudflare.ZoneCustomSSLPriority, error) { + data, dataOk := d.GetOk("custom_ssl_priority") + log.Printf("[DEBUG] Custom SSL priority found in config: %#v", data) + var mtSlice []cloudflare.ZoneCustomSSLPriority + if dataOk { + for _, innerData := range data.([]interface{}) { + newData := make(map[string]interface{}) + for id, value := range innerData.(map[string]interface{}) { + switch idName := id; idName { + case "id": + newValue := value.(string) + newData["ID"] = newValue + case "priority": + newValue := value.(int) + newData[id] = newValue + default: + newValue := value + newData[id] = newValue + } + } + zcsp := cloudflare.ZoneCustomSSLPriority{} + zcspJSON, err := json.Marshal(newData) + if err != nil { + return mtSlice, fmt.Errorf("Failed to create custom ssl priorities: %s", err) + } + // map -> json -> struct + json.Unmarshal(zcspJSON, &zcsp) + mtSlice = append(mtSlice, zcsp) + } + } + log.Printf("[DEBUG] Custom SSL priority list creating: %#v", mtSlice) + return mtSlice, nil +} + +func expandToZoneCustomSSLOptions(d *schema.ResourceData) (cloudflare.ZoneCustomSSLOptions, error) { + data, dataOk := d.GetOk("custom_ssl_options") + log.Printf("[DEBUG] Custom SSL options found in config: %#v", data) + + newData := make(map[string]interface{}) + if dataOk { + for id, value := range data.(map[string]interface{}) { + var newValue interface{} + if id == "geo_restrictions" { + newValue = cloudflare.ZoneCustomSSLGeoRestrictions{ + Label: value.(string), + } + } else { + newValue = value.(string) + } + newData[id] = newValue + } + } + + zcso := cloudflare.ZoneCustomSSLOptions{} + zcsoJSON, err := json.Marshal(newData) + if err != nil { + return zcso, fmt.Errorf("Failed to create custom ssl options: %s", err) + } + + log.Printf("[DEBUG] Custom SSL JSON: %s", string(zcsoJSON)) + + // map -> json -> struct + json.Unmarshal(zcsoJSON, &zcso) + log.Printf("[DEBUG] Custom SSL options creating: %#v", zcso) + return zcso, nil +} + +func flattenCustomSSLOptions(sslopt cloudflare.ZoneCustomSSLOptions) map[string]interface{} { + data := map[string]interface{}{ + "certificate": sslopt.Certificate, + "private_key": sslopt.PrivateKey, + "bundle_method": sslopt.BundleMethod, + "geo_restrictions": sslopt.GeoRestrictions.Label, + } + return data +} diff --git a/cloudflare/resource_cloudflare_custom_ssl_test.go b/cloudflare/resource_cloudflare_custom_ssl_test.go new file mode 100644 index 0000000000..e6498bfaee --- /dev/null +++ b/cloudflare/resource_cloudflare_custom_ssl_test.go @@ -0,0 +1,103 @@ +package cloudflare + +import ( + "fmt" + "os" + "regexp" + "testing" + + cloudflare "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccCloudflareCustomSSL_Basic(t *testing.T) { + t.Parallel() + var customSSL cloudflare.ZoneCustomSSL + zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") + rnd := generateRandomResourceName() + resourceName := "cloudflare_custom_ssl." + rnd + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudflareCustomSSLDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckCloudflareCustomSSLCertBasic(zoneID, rnd), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareCustomSSLExists(resourceName, &customSSL), + resource.TestCheckResourceAttr( + resourceName, "zone_id", zoneID), + resource.TestMatchResourceAttr( + resourceName, "priority", regexp.MustCompile("^[0-9]\\d*$")), + resource.TestCheckResourceAttr( + resourceName, "status", "active"), + resource.TestMatchResourceAttr( + resourceName, "zone_id", regexp.MustCompile("^[a-z0-9]{32}$")), + resource.TestCheckResourceAttr( + resourceName, "custom_ssl_options.bundle_method", "ubiquitous"), + resource.TestCheckResourceAttr( + resourceName, "custom_ssl_options.type", "legacy_custom"), + ), + }, + }, + }) +} + +func testAccCheckCloudflareCustomSSLCertBasic(zoneID string, rName string) string { + return fmt.Sprintf(` +resource "cloudflare_custom_ssl" "%[2]s" { + zone_id = "%[1]s" + custom_ssl_options = { + certificate = "-----BEGIN CERTIFICATE-----\nMIIFXjCCBEagAwIBAgISAympguRfAsX307ZikP7jl0dwMA0GCSqGSIb3DQEBCwUA\nMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD\nExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xOTA4MDUxNDU4NTlaFw0x\nOTExMDMxNDU4NTlaMB4xHDAaBgNVBAMTE3RlcnJhZm9ybS5jZmFwaS5uZXQwggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDMmJ7n1plIwwuA45Q6GeAb13A6\n4/1rOUIHYp+wG/IRyUQijpoSldeYd9pzJOudB200tmNnmMmYIIT7xyewWcEw2LHP\nyEJ2vIuZnlGVDSL9PYrD3T24XRZk4A70wXr9FXxDHAIr+QgGY8YCk9Jv88ySIXIh\nxEf6w/5HYUR46sq+F97QA4w8OhcQCJ5t35ujt9LBxiDlBuh4vDuj4TgkQvtAwB2A\nY+sP+r9Taj5syX2fxGw/OwXqYn+JLoWo52s3790VG3sAUPZ2IoIz+rvs1MAkAer+\nicV6UGSRhBDifg6dZKJmlIJ+I2Jk049wI8imQGLFgM+FMWSoHkRCP9L+obTBAgMB\nAAGjggJoMIICZDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG\nCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFLx2OSHdcqidsz29u2Ly\numoF9Wb0MB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUF\nBwEBBGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNy\neXB0Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNy\neXB0Lm9yZy8wHgYDVR0RBBcwFYITdGVycmFmb3JtLmNmYXBpLm5ldDBMBgNVHSAE\nRTBDMAgGBmeBDAECATA3BgsrBgEEAYLfEwEBATAoMCYGCCsGAQUFBwIBFhpodHRw\nOi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB1\nAHR+2oMxrTMQkSGcziVPQnDCv/1eQiAIxjc1eeYQe8xWAAABbGKBV4oAAAQDAEYw\nRAIgfMVR6qQguG+a0LmXkDdAVGyVLafenFcJch7qVLPVJiACIEB6Utb1Cuts4Q3P\ndq2c7Srp2OWUwEzGUCjcoYduckg0AHcAKTxRllTIOWW6qlD8WAfUt2+/WHopctyk\nwwz05UVH9HgAAAFsYoFZnAAABAMASDBGAiEA7JnUEyzKzxWvleuyRbQO/e8FYijl\nM7uMRTkI9pbUGUgCIQCUSooINNU5zDYH0a+z/C1ubTGVo6edcj+mmsuqG37RIjAN\nBgkqhkiG9w0BAQsFAAOCAQEAfiBetJTZ51lfQ7GJSXCrejYpzDklz5OpbvR1uHb0\nqiP/G8trGqXyPyjhFGZlFMOag0VQYcsmhEvDCveV67bgziHRthCkPNXMveKHYDRw\njvifAOf1LSRWvzHEKsAyTb5s/qnSOnmH8U2bE2zn45W65ztYjQGJAx8MA908Dcrx\nRoMcVKqmfXVePNY3w8DCZJrc2O17Q8BABjaQvxm2HT48eSD8G+fUoaFIa6nu/FUs\nMustdz8mZg2boAZ1J0yNxa80Y1s44H3kOSYxfgfKYoe1+GGcUKbdM/EF7v4tzYNj\nT11Xy/i+xFc5fa+H2Gpycit3fdkj5TEVPX7ye4yHo2wtPg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow\nSjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT\nGkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF\nq6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8\nSMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0\nZ8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA\na6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj\n/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T\nAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG\nCCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv\nbTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k\nc3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw\nVAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC\nARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz\nMDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu\nY3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF\nAAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo\nuM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/\nwApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu\nX4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG\nPfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6\nKOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==\n-----END CERTIFICATE-----\n" + private_key = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMmJ7n1plIwwuA\n45Q6GeAb13A64/1rOUIHYp+wG/IRyUQijpoSldeYd9pzJOudB200tmNnmMmYIIT7\nxyewWcEw2LHPyEJ2vIuZnlGVDSL9PYrD3T24XRZk4A70wXr9FXxDHAIr+QgGY8YC\nk9Jv88ySIXIhxEf6w/5HYUR46sq+F97QA4w8OhcQCJ5t35ujt9LBxiDlBuh4vDuj\n4TgkQvtAwB2AY+sP+r9Taj5syX2fxGw/OwXqYn+JLoWo52s3790VG3sAUPZ2IoIz\n+rvs1MAkAer+icV6UGSRhBDifg6dZKJmlIJ+I2Jk049wI8imQGLFgM+FMWSoHkRC\nP9L+obTBAgMBAAECggEAPs7voW6A2hR+eI/k1j1RTlrB6mJJTtxiB9BgA3lgw9MM\npqsuY1w6tmS83DJOXoOEI/WF6Ky/3oLFMGIALiQvqaYsWAQ7WyYgmQVAOEizIBj/\ne4d0xh9Vm5wpGzw2XHF3F0cG56borsV8aRgmNxYaDBZWakVOb44xhoo2sgQqP1aZ\nrSYUPmLfC2vDyIUiNWZ1f8IJH7IwcvLkaGSzSn8BW/euCz+4B+vz8x3smWKPFM0Q\nkXcL9ciJ1GuDpjhlg006MO0mtcvG76OHX+qPMny1sIFN4villOaHz4SFgYLGRB72\n7Lqyuga+eRrZFarquDkIxSzHdQkesnsK/pKt5GDdUQKBgQD3pXU7X1tDY88mODVg\n3yB/F1V9hgEQZ2N8SGsj8eOhq8AkZqzr9oog/fwYYcgxH1uDn2ESivK/ggGq66ge\nDq9cLzfvOyXaHyLR5DzkdXP3gLoDgH4MTrY9jGcP5Tf6eyUjI+qvDvb+A4qOkVlN\nCH9VA/4zCkM4GiYyWSjw1jpIpQKBgQDTf2eUAMBfRGjQZedfjAxxRb7x6K7VXlME\nW4LBnRZaPnlmm0oPbeDmNlvxmi1BXP8Js2nbY8GmCGe0EtFKZB4qxy5HxbJVzCU5\npt2ZnOp4limGwkEBb/SjdoII0HsNfjddnwvpYx4Vaj0uHH2zF6SqenNlzmg3UwvP\n4FbXr1xk7QKBgQCRKOQ5xCBLtSKEZagsOz3iITxUUosnIWM4Q37B2BS0/GapL6Im\nwiGfSyFM7WwaFyZeVbrh0p6N0NfHZ1DpJXR21Zq02PfMDjory9xBkfNC3aqrSNMZ\nxb2fAECdGaAha7OOEIyMxnnS1SKPhPVSaSuyGqATLO3P4cwH8SlFWl1ZnQKBgGLH\noYfVpgOYvt9+iMbucS1CZwEzLN0I1fs2BmcJSFRT032hz8BPEHhVMTIxUSuzFIbi\nXfGSsPIsAMtw8oEtK43NQ4dQBY/e7g/0KJHDYRt6/uAqwBO8x2TFR8x4GtDdf1xh\nmT2jBnz4BqUPt4G67DSXRmhpM/GK/vxTChxokd2tAoGAYX4s3UGcSQ8U/zxf07y5\n0Z+/5aMqobiUPwX3OAPrJ0MDDelyCtoZzaZ3OrI1/E6DQlUHAgdxvMC5OnP97IU7\nH8akcLR6bN6L9q1CinCJQMx+6fs78qenmaknvVFW5mk35tyEpS6yqauIf1Ddrqxa\n3MA/2EkI8J7LVX1KcuTUGTQ=\n-----END PRIVATE KEY-----\n" + bundle_method = "ubiquitous", + geo_restrictions = "us" + type = "legacy_custom" + } +}`, zoneID, rName) +} + +func testAccCheckCloudflareCustomSSLDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*cloudflare.API) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudflare_custom_ssl" { + continue + } + + err := client.DeleteSSL(rs.Primary.Attributes["zone_id"], rs.Primary.ID) + if err == nil { + return fmt.Errorf("cert still exists") + } + } + + return nil +} + +func testAccCheckCloudflareCustomSSLExists(n string, customSSL *cloudflare.ZoneCustomSSL) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No cert ID is set") + } + + client := testAccProvider.Meta().(*cloudflare.API) + foundCustomSSL, err := client.SSLDetails(rs.Primary.Attributes["zone_id"], rs.Primary.ID) + if err != nil { + return err + } + + if foundCustomSSL.ID != rs.Primary.ID { + return fmt.Errorf("cert not found") + } + + *customSSL = foundCustomSSL + + return nil + } +} diff --git a/website/docs/r/custom_ssl.html.markdown b/website/docs/r/custom_ssl.html.markdown new file mode 100644 index 0000000000..b7c3a21be0 --- /dev/null +++ b/website/docs/r/custom_ssl.html.markdown @@ -0,0 +1,56 @@ +--- +layout: "cloudflare" +page_title: "Cloudflare: cloudflare_custom_ssl" +sidebar_current: "docs-cloudflare-resource-custom-ssl" +description: !- + Provides a Cloudflare custom ssl resource. +--- + +# cloudflare_custom_ssl + +Provides a Cloudflare custom ssl resource. + +## Example Usage + +```hcl +# Add a custom ssl certificate to the domain +resource "cloudflare_custom_ssl" "foossl" { + zone_id = "${var.cloudflare_zone_id}" + custom_ssl_options = { + "certificate" = "-----INSERT CERTIFICATE-----" + "private_key" = "-----INSERT PRIVATE KEY-----" + "bundle_method" = "ubiquitous", + "geo_restrictions" = "us", + "type" = "legacy_custom" + } +} + +variable "cloudflare_zone_id" { + type = "string" + default = "1d5fdc9e88c8a8c4518b068cd94331fe" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `zone_id` - (Required) The DNS zone id to the custom ssl cert should be added. +* `custom_ssl_options` - (Required) The certificate, private key and associated optional parameters, such as bundle_method, geo_restrictions, and type. + +**custom_ssl_options** block supports: + +* `certificate` - (Required) Certificate certificate and the intermediate(s) +* `private_key` - (Required) Certificate's private key +* `bundle_method` - (Optional) Method of building intermediate certificate chain. A ubiquitous bundle has the highest probability of being verified everywhere, even by clients using outdated or unusual trust stores. An optimal bundle uses the shortest chain and newest intermediates. And the force bundle verifies the chain, but does not otherwise modify it. Valid values are `ubiquitous` (default), `optimal`, `force`. +* `geo_restrictions` - (Optional) Specifies the region where your private key can be held locally. Valid values are `us`, `eu`, `highest_security`. +* `type` - (Optional) Whether to enable support for legacy clients which do not include SNI in the TLS handshake. Valid values are `legacy_custom` (default), `sni_custom`. + +## Import + +Custom SSL Certs can be imported using a composite ID formed of the zone id and certificate id, +separated by a "/" e.g. + +``` +$ terraform import cloudflare_custom_ssl.default 1d5fdc9e88c8a8c4518b068cd94331fe/c671356fb0ef68a9d746e3c9ef84ec3e +```