Skip to content

Commit

Permalink
add managed headers resource
Browse files Browse the repository at this point in the history
  • Loading branch information
Denis Davydov committed Jun 9, 2022
1 parent 9648b75 commit 4d807de
Show file tree
Hide file tree
Showing 7 changed files with 360 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .changelog/myPRnumber.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/cloudflare_managed_headers: Add support of Managed Headers API.
```
23 changes: 23 additions & 0 deletions examples/resources/managed_headers/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Enable security headers using Managed Meaders
resource "cloudflare_managed_headers" "add_security_headers_example" {
zone_id = "cb029e245cfdd66dc8d2e570d5dd3322"
managed_request_headers {
id = "add_true_client_ip_headers"
enabled = true
}

managed_request_headers {
id = "add_visitor_location_headers"
enabled = false
}

managed_response_headers {
id = "add_security_headers"
enabled = false
}

managed_response_headers {
id = "remove_x-powered-by_header"
enabled = true
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/bflad/tfproviderlint v0.28.1
github.com/client9/misspell v0.3.4
github.com/cloudflare/cloudflare-go v0.41.0
github.com/cloudflare/cloudflare-go v0.41.1-0.20220608232151-270cf737aeba
github.com/fatih/color v1.13.0 // indirect
github.com/golangci/golangci-lint v1.46.2
github.com/google/go-github v17.0.0+incompatible
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.41.0 h1:4HiWuBpBj1fMiyWDfIlLGxnuPuEbkLi+SZWq9tgCFfc=
github.com/cloudflare/cloudflare-go v0.41.0/go.mod h1:o0jm+vdFrhwy7GOT3PB/71JQ6kElUQcifPc2Z9KTxeE=
github.com/cloudflare/cloudflare-go v0.41.1-0.20220608232151-270cf737aeba h1:kfXdddUwNKsuqOHXJuGMnSY956aLmt9z1THZF8o95oI=
github.com/cloudflare/cloudflare-go v0.41.1-0.20220608232151-270cf737aeba/go.mod h1:o0jm+vdFrhwy7GOT3PB/71JQ6kElUQcifPc2Z9KTxeE=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
Expand Down
173 changes: 173 additions & 0 deletions internal/provider/resource_cloudflare_managed_headers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package provider

import (
"context"
"errors"
"fmt"

cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceCloudflareManagedHeaders() *schema.Resource {
return &schema.Resource{
Schema: resourceCloudflareManagedHeadersSchema(),
CreateContext: resourceCloudflareManagedHeadersCreate,
ReadContext: resourceCloudflareManagedHeadersRead,
UpdateContext: resourceCloudflareManagedHeadersUpdate,
DeleteContext: resourceCloudflareManagedHeadersDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceCloudflareManagedHeadersImport,
},
SchemaVersion: 0,
Description: `
The [Cloudflare Managed Headers](https://developers.cloudflare.com/rules/transform/managed-transforms/)
allows you to add or remove some predefined headers to one's requests or origin responses.
~> **NOTE:** You can configure Managed Headers using the dashboard (https://api.cloudflare.com/#managed-headers-api-properties)
Terraform will override yours configuration if it exists.`,
}
}

func resourceCloudflareManagedHeadersCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
zoneID := d.Get("zone_id").(string)
d.SetId(zoneID)
return resourceCloudflareManagedHeadersUpdate(ctx, d, meta)
}

func resourceCloudflareManagedHeadersRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
zoneID := d.Get("zone_id").(string)
headers, err := client.ListZoneManagedHeaders(ctx, cloudflare.ListManagedHeadersParams{
ZoneID: zoneID,
})
if err != nil {
return diag.FromErr(fmt.Errorf("error reading managed headers: %w", err))
}

if err := d.Set("managed_request_headers", buildResourceFromManagedHeaders(headers.ManagedRequestHeaders)); err != nil {
return diag.FromErr(err)
}
if err := d.Set("managed_response_headers", buildResourceFromManagedHeaders(headers.ManagedResponseHeaders)); err != nil {
return diag.FromErr(err)
}
return nil
}

func buildResourceFromManagedHeaders(headers []cloudflare.ManagedHeader) interface{} {
headersState := []map[string]interface{}{}
for _, header := range headers {
headersState = append(headersState, map[string]interface{}{
"id": header.ID,
"enabled": header.Enabled,
})
}

return headersState
}

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

mh, err := buildManagedHeadersFromResource(d)
if err != nil {
return diag.FromErr(fmt.Errorf("error building managed headers from resource: %w", err))
}
if _, err := client.UpdateZoneManagedHeaders(ctx, cloudflare.UpdateManagedHeadersParams{
ManagedHeaders: mh,
ZoneID: zoneID,
}); err != nil {
return diag.FromErr(fmt.Errorf("error updating managed headers: %w", err))
}
return resourceCloudflareManagedHeadersRead(ctx, d, meta)
}

// receives the resource config and builds a managed headers struct.
func buildManagedHeadersFromResource(d *schema.ResourceData) (cloudflare.ManagedHeaders, error) {
requestHeaders, ok := d.Get("managed_request_headers").([]interface{})
if !ok {
return cloudflare.ManagedHeaders{}, errors.New("unable to create interface array type assertion")
}
reqHeaders, err := buildManagedHeadersListFromResource(requestHeaders)
if err != nil {
return cloudflare.ManagedHeaders{}, err
}

responseHeaders, ok := d.Get("managed_response_headers").([]interface{})
if !ok {
return cloudflare.ManagedHeaders{}, errors.New("unable to create interface array type assertion")
}
respHeaders, err := buildManagedHeadersListFromResource(responseHeaders)
if err != nil {
return cloudflare.ManagedHeaders{}, err
}

return cloudflare.ManagedHeaders{
ManagedRequestHeaders: reqHeaders,
ManagedResponseHeaders: respHeaders,
}, nil
}

func buildManagedHeadersListFromResource(resource []interface{}) ([]cloudflare.ManagedHeader, error) {
headers := make([]cloudflare.ManagedHeader, 0, len(resource))
for _, header := range resource {
h, ok := header.(map[string]interface{})
if !ok {
return nil, errors.New("unable to create interface map type assertion for managed header")
}
id, ok := h["id"].(string)
if !ok {
return nil, errors.New("unable to create string type assertion for managed header ID")
}
enabled, ok := h["enabled"].(bool)
if !ok {
return nil, errors.New("unable to create bool type assertion for managed header enabled")
}
headers = append(headers, cloudflare.ManagedHeader{
ID: id,
Enabled: enabled,
})
}
return headers, nil
}

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

headers, err := client.ListZoneManagedHeaders(ctx, cloudflare.ListManagedHeadersParams{
ZoneID: zoneID,
})
if err != nil {
return diag.FromErr(fmt.Errorf("error reading managed headers: %w", err))
}

requestHeaders := make([]cloudflare.ManagedHeader, 0, len(headers.ManagedRequestHeaders))
for _, header := range headers.ManagedRequestHeaders {
header.Enabled = false
requestHeaders = append(requestHeaders, header)
}
responseHeaders := make([]cloudflare.ManagedHeader, 0, len(headers.ManagedResponseHeaders))
for _, header := range headers.ManagedResponseHeaders {
header.Enabled = false
responseHeaders = append(responseHeaders, header)
}

if _, err := client.UpdateZoneManagedHeaders(ctx, cloudflare.UpdateManagedHeadersParams{
ManagedHeaders: cloudflare.ManagedHeaders{
ManagedRequestHeaders: requestHeaders,
ManagedResponseHeaders: responseHeaders,
},
ZoneID: zoneID,
}); err != nil {
return diag.FromErr(fmt.Errorf("error deleting managed headers with ID %q: %w", d.Id(), err))
}

return nil
}

func resourceCloudflareManagedHeadersImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
return nil, errors.New("Import is not yet supported for Managed Headers")
}
107 changes: 107 additions & 0 deletions internal/provider/resource_cloudflare_managed_headers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package provider

import (
"context"
"fmt"
"os"
"testing"

cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/pkg/errors"
)

func init() {
resource.AddTestSweepers("cloudflare_managed_headers", &resource.Sweeper{
Name: "cloudflare_managed_headers",
F: testSweepCloudflareManagedHeaders,
})
}

func testSweepCloudflareManagedHeaders(r string) error {
ctx := context.Background()
client, clientErr := sharedClient()
if clientErr != nil {
tflog.Error(ctx, fmt.Sprintf("Failed to create Cloudflare client: %s", clientErr))
}

zone := os.Getenv("CLOUDFLARE_ZONE_ID")
if zone == "" {
return errors.New("CLOUDFLARE_ZONE_ID must be set")
}

managedHeaders, err := client.ListZoneManagedHeaders(context.Background(), cloudflare.ListManagedHeadersParams{
ZoneID: zoneID,
})
if err != nil {
tflog.Error(ctx, fmt.Sprintf("Failed to fetch Cloudflare Zone Managed Headers: %s", err))
}

requestHeaders := make([]cloudflare.ManagedHeader, 0, len(managedHeaders.ManagedRequestHeaders))
for _, h := range managedHeaders.ManagedRequestHeaders {
tflog.Info(ctx, fmt.Sprintf("Disabling Cloudflare Zone Managed Header ID: %s", h.ID))
h.Enabled = false
requestHeaders = append(requestHeaders, h)
}
responseHeaders := make([]cloudflare.ManagedHeader, 0, len(managedHeaders.ManagedResponseHeaders))
for _, h := range managedHeaders.ManagedResponseHeaders {
tflog.Info(ctx, fmt.Sprintf("Disabling Cloudflare Zone Managed Header ID: %s", h.ID))
h.Enabled = false
responseHeaders = append(responseHeaders, h)
}

_, err = client.UpdateZoneManagedHeaders(context.Background(), cloudflare.UpdateManagedHeadersParams{
ManagedHeaders: cloudflare.ManagedHeaders{
ManagedRequestHeaders: requestHeaders,
ManagedResponseHeaders: responseHeaders,
},
ZoneID: zoneID,
})
if err != nil {
tflog.Error(ctx, fmt.Sprintf("Failed to disable Cloudflare Zone Managed Headers: %s", err))
}

return nil
}

func TestZoneCloudflareManagedHeaders(t *testing.T) {
t.Parallel()

rnd := generateRandomResourceName()
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
resourceName := "cloudflare_managed_headers." + rnd
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: testAccCheckCloudflareManagedHeaders(rnd, zoneID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "zone_id", zoneID),
resource.TestCheckResourceAttr(resourceName, "managed_request_headers.#", "1"),
resource.TestCheckResourceAttr(resourceName, "managed_request_headers.0.id", "add_true_client_ip_headers"),
resource.TestCheckResourceAttr(resourceName, "managed_request_headers.0.enabled", "true"),
resource.TestCheckResourceAttr(resourceName, "managed_response_headers.#", "1"),
resource.TestCheckResourceAttr(resourceName, "managed_response_headers.0.id", "add_security_headers"),
resource.TestCheckResourceAttr(resourceName, "managed_response_headers.0.enabled", "true"),
),
},
},
})
}

func testAccCheckCloudflareManagedHeaders(rnd, zoneID string) string {
return fmt.Sprintf(`
resource "cloudflare_managed_headers" "%[1]s" {
zone_id = "%[2]s"
managed_request_headers {
id = "add_true_client_ip_headers"
enabled = true
}
managed_response_headers {
id = "add_security_headers"
enabled = true
}
}`, rnd, zoneID)
}
51 changes: 51 additions & 0 deletions internal/provider/schema_cloudflare_managed_headers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package provider

import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

func resourceCloudflareManagedHeadersSchema() map[string]*schema.Schema {
return map[string]*schema.Schema{
"zone_id": {
Description: "The zone identifier to target for the resource.",
Type: schema.TypeString,
Optional: false,
},
"managed_request_headers": {
Description: "The list of managed request headers",
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Computed: true,
Description: "Unique headers rule identifier.",
},
"enabled": {
Type: schema.TypeBool,
Optional: true,
Description: "Whether the headers rule is active.",
},
},
},
},
"managed_response_headers": {
Description: "The list of managed response headers",
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Computed: true,
Description: "Unique headers rule identifier.",
},
"enabled": {
Type: schema.TypeBool,
Optional: true,
Description: "Whether the headers rule is active.",
},
},
},
},
}
}

0 comments on commit 4d807de

Please sign in to comment.