Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Keyless certificate resource #2779

Merged
merged 23 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/2779.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
cloudflare_keyless_certificate
```
52 changes: 52 additions & 0 deletions docs/resources/keyless_certificate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
page_title: "cloudflare_keyless_certificate Resource - Cloudflare"
subcategory: ""
description: |-
Provides a resource, that manages Keyless certificates.
---

# cloudflare_keyless_certificate (Resource)

Provides a resource, that manages Keyless certificates.

## Example Usage

```terraform
resource "cloudflare_keyless_certificate" "example" {
zone_id = "0da42c8d2132a9ddaf714f9e7c920711"
bundle_method = "ubiquitous"
name = "example.com Keyless SSL"
host = "example.com"
port = 24008
enabled = true
certificate = "-----INSERT CERTIFICATE-----"
}
```
<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `certificate` (String) The zone's SSL certificate or SSL certificate and intermediate(s). **Modifying this attribute will force creation of a new resource.**
- `host` (String) The KeyLess SSL host.
- `zone_id` (String) The zone identifier to target for the resource.

### Optional

- `bundle_method` (String) 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. Available values: `ubiquitous`, `optimal`, `force`. Defaults to `ubiquitous`. **Modifying this attribute will force creation of a new resource.**
- `enabled` (Boolean) Whether the KeyLess SSL is on.
- `name` (String) The KeyLess SSL name.
- `port` (Number) The KeyLess SSL port used to communicate between Cloudflare and the client's KeyLess SSL server. Defaults to `24008`.

### Read-Only

- `id` (String) The ID of this resource.
- `status` (String) Status of the KeyLess SSL.

## Import

Import is supported using the following syntax:

```shell
$ terraform import cloudflare_keyless_certificate.example <zone_id>/<keyless_certificate_id>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$ terraform import cloudflare_keyless_certificate.example <zone_id>/<keyless_certificate_id>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
resource "cloudflare_keyless_certificate" "example" {
zone_id = "0da42c8d2132a9ddaf714f9e7c920711"
bundle_method = "ubiquitous"
name = "example.com Keyless SSL"
host = "example.com"
port = 24008
enabled = true
certificate = "-----INSERT CERTIFICATE-----"
}
1 change: 1 addition & 0 deletions internal/sdkv2provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ func New(version string) func() *schema.Provider {
"cloudflare_hostname_tls_setting": resourceCloudflareHostnameTLSSetting(),
"cloudflare_hostname_tls_setting_ciphers": resourceCloudflareHostnameTLSSettingCiphers(),
"cloudflare_ipsec_tunnel": resourceCloudflareIPsecTunnel(),
"cloudflare_keyless_certificate": resourceCloudflareKeylessCertificate(),
"cloudflare_list_item": resourceCloudflareListItem(),
"cloudflare_list": resourceCloudflareList(),
"cloudflare_load_balancer_monitor": resourceCloudflareLoadBalancerMonitor(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log"
"os"
"testing"
"time"

cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
Expand Down Expand Up @@ -92,7 +93,8 @@ func TestAccCloudflareCustomHostname_WithCertificate(t *testing.T) {
rnd := generateRandomResourceName()
resourceName := "cloudflare_custom_hostname." + rnd

cert, key, err := utils.GenerateEphemeralCertAndKey([]string{rnd + "." + domain})
expiry := time.Now().Add(time.Hour * 1)
cert, key, err := utils.GenerateEphemeralCertAndKey([]string{rnd + "." + domain}, expiry)
if err != nil {
t.Error(err)
}
Expand Down
146 changes: 146 additions & 0 deletions internal/sdkv2provider/resource_cloudflare_keyless_certificate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package sdkv2provider

import (
"context"
"fmt"
"strings"

"github.com/MakeNowJust/heredoc/v2"
cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/pkg/errors"
)

func resourceCloudflareKeylessCertificate() *schema.Resource {
return &schema.Resource{
Schema: resourceCloudflareKeylessCertificateSchema(),
CreateContext: resourceCloudflareKeylessCertificateCreate,
ReadContext: resourceCloudflareKeylessCertificateRead,
UpdateContext: resourceCloudflareKeylessCertificateUpdate,
DeleteContext: resourceCloudflareKeylessCertificateDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceCloudflareKeylessCertificateImport,
},
Description: heredoc.Doc(`
Provides a resource, that manages Keyless certificates.
`),
}
}

func resourceCloudflareKeylessCertificateCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
zoneID := d.Get(consts.ZoneIDSchemaKey).(string)

request := cloudflare.KeylessSSLCreateRequest{
Name: d.Get("name").(string),
Host: d.Get("host").(string),
Port: d.Get("port").(int),
Certificate: d.Get("certificate").(string),
BundleMethod: d.Get("bundle_method").(string),
}

res, err := client.CreateKeylessSSL(ctx, zoneID, request)

if err != nil {
return diag.FromErr(errors.Wrap(err, fmt.Sprintf("failed to create Keyless SSL")))
}

retry := retry.RetryContext(ctx, d.Timeout(schema.TimeoutCreate), func() *retry.RetryError {
keylessSSL, err := client.KeylessSSL(ctx, zoneID, res.ID)
if err != nil {
return retry.NonRetryableError(fmt.Errorf("failed to fetch keyless certificate: %w", err))
}

if keylessSSL.Status != "active" {
return retry.RetryableError(fmt.Errorf("waiting for the keyless certificate to become active"))
}

d.SetId(res.ID)

resourceCloudflareKeylessCertificateRead(ctx, d, meta)
return nil
})

if retry != nil {
return diag.FromErr(retry)
}

return nil
}

func resourceCloudflareKeylessCertificateRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
zoneID := d.Get(consts.ZoneIDSchemaKey).(string)

keylessSSL, err := client.KeylessSSL(ctx, zoneID, d.Id())
if err != nil {
var notFoundError *cloudflare.NotFoundError
if errors.As(err, &notFoundError) {
tflog.Info(ctx, fmt.Sprintf("Keyless SSL %s no longer exists", d.Id()))
d.SetId("")
return nil
}
return diag.FromErr(fmt.Errorf("error finding Keyless SSL %q: %w", d.Id(), err))
}

d.Set("name", keylessSSL.Name)
d.Set("host", keylessSSL.Host)
d.Set("port", keylessSSL.Port)
d.Set("status", keylessSSL.Status)
d.Set("enabled", keylessSSL.Enabled)
d.Set("port", keylessSSL.Port)
return nil
}

func resourceCloudflareKeylessCertificateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
zoneID := d.Get(consts.ZoneIDSchemaKey).(string)

request := cloudflare.KeylessSSLUpdateRequest{
Name: d.Get("name").(string),
Host: d.Get("host").(string),
Port: d.Get("port").(int),
Enabled: cloudflare.BoolPtr(d.Get("enabled").(bool)),
}

_, err := client.UpdateKeylessSSL(ctx, zoneID, d.Id(), request)
if err != nil {
return diag.FromErr(errors.Wrap(err, fmt.Sprintf("failed to update Keyless SSL")))
}

return resourceCloudflareKeylessCertificateRead(ctx, d, meta)
}

func resourceCloudflareKeylessCertificateDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
zoneID := d.Get(consts.ZoneIDSchemaKey).(string)

err := client.DeleteKeylessSSL(ctx, zoneID, d.Id())
if err != nil {
return diag.FromErr(errors.Wrap(err, fmt.Sprintf("failed to delete Keyless SSL")))
}

return nil
}

func resourceCloudflareKeylessCertificateImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
idAttr := strings.SplitN(d.Id(), "/", 2)
if len(idAttr) != 2 {
return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"zoneID/KeylessSSLID\"", d.Id())
}

zoneID, keylessSslId := idAttr[0], idAttr[1]

tflog.Debug(ctx, fmt.Sprintf("Importing Cloudflare Keyless SSL: id %s for zone %s", keylessSslId, zoneID))

d.Set(consts.ZoneIDSchemaKey, zoneID)
d.SetId(keylessSslId)

resourceCloudflareKeylessCertificateRead(ctx, d, meta)

return []*schema.ResourceData{d}, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package sdkv2provider

import (
"context"
"errors"
"fmt"
"os"
"testing"
"time"

cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/cloudflare/terraform-provider-cloudflare/internal/utils"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
)

func TestAccCloudflareKeylessSSL_Basic(t *testing.T) {
t.Parallel()
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_keyless_certificate.%s", rnd)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
CheckDestroy: testAccCheckCloudflareKeylessCertificateDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudflareKeylessCertificate(rnd, zoneID, domain),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, consts.ZoneIDSchemaKey, zoneID),
resource.TestCheckResourceAttr(name, "port", "24008"),
resource.TestCheckResourceAttr(name, "enabled", "false"),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "host", domain),
),
},
},
})
}

func testAccCheckCloudflareKeylessCertificateDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*cloudflare.API)

for _, rs := range s.RootModule().Resources {
if rs.Type != "cloudflare_keyless_certificate" {
continue
}

ctx := context.Background()
err := retry.RetryContext(ctx, time.Second*10, func() *retry.RetryError {
keylessCertificates, err := client.ListKeylessSSL(ctx, zoneID)
if err != nil {
return retry.NonRetryableError(fmt.Errorf("failed to fetch keyless certificate: %w", err))
}

for _, keylessCertificate := range keylessCertificates {
if keylessCertificate.ID == rs.Primary.Attributes["id"] {
return retry.RetryableError(fmt.Errorf("keyless certificate cleanup is processing"))
}
}

return nil
})
if err != nil {
return errors.New("failed to initiate retries for Keyless SSL deletion")
}
}

return nil
}

func testAccCloudflareKeylessCertificate(resourceName, zoneId string, domain string) string {
expiry := time.Now().Add(time.Hour * 730)
cert, _, _ := utils.GenerateEphemeralCertAndKey([]string{domain}, expiry)

return fmt.Sprintf(`
resource "cloudflare_keyless_certificate" "%[1]s" {
zone_id = "%[2]s"
bundle_method = "force"
name = "%[1]s"
host = "%[3]s"
port = 24008
certificate = <<EOT
%[4]s
EOT
}`, resourceName, zoneId, domain, cert)
}
59 changes: 59 additions & 0 deletions internal/sdkv2provider/schema_cloudflare_keyless_certificate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package sdkv2provider

import (
"fmt"

"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

func resourceCloudflareKeylessCertificateSchema() map[string]*schema.Schema {
return map[string]*schema.Schema{
consts.ZoneIDSchemaKey: {
Description: consts.ZoneIDSchemaDescription,
Type: schema.TypeString,
Required: true,
},
"bundle_method": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "ubiquitous",
Description: fmt.Sprintf("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. %s", renderAvailableDocumentationValuesStringSlice([]string{"ubiquitous", "optimal", "force"})),
},
"certificate": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "The zone's SSL certificate or SSL certificate and intermediate(s).",
},
"host": {
Type: schema.TypeString,
Required: true,
Description: "The KeyLess SSL host.",
},
"name": {
Type: schema.TypeString,
Optional: true,
Description: "The KeyLess SSL name.",
},
"port": {
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntBetween(0, 65535),
Default: 24008,
Description: "The KeyLess SSL port used to communicate between Cloudflare and the client's KeyLess SSL server.",
},
"enabled": {
Type: schema.TypeBool,
Optional: true,
Description: "Whether the KeyLess SSL is on.",
},
"status": {
Description: "Status of the KeyLess SSL.",
Type: schema.TypeString,
Computed: true,
},
}
}
Loading
Loading