From 357205024787c877acc8e3f367a683e213f1cde3 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Wed, 24 Oct 2018 15:08:43 +1100 Subject: [PATCH] Add support for `cloudflare_custom_pages` resource This introduces support for managing the custom error pages as a Terraform resource. Depends on cloudflare/cloudflare-go#240 however I will wait for that to land and then propose a PR that updates the vendored library in a separate PR. --- cloudflare/provider.go | 1 + .../resource_cloudflare_custom_pages.go | 175 ++++++++++++++++++ cloudflare/validators.go | 12 ++ website/cloudflare.erb | 3 + website/docs/r/custom_pages.html.markdown | 60 ++++++ 5 files changed, 251 insertions(+) create mode 100644 cloudflare/resource_cloudflare_custom_pages.go create mode 100644 website/docs/r/custom_pages.html.markdown diff --git a/cloudflare/provider.go b/cloudflare/provider.go index c22da224f3..8b1655d09b 100644 --- a/cloudflare/provider.go +++ b/cloudflare/provider.go @@ -88,6 +88,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "cloudflare_access_rule": resourceCloudflareAccessRule(), "cloudflare_account_member": resourceCloudflareAccountMember(), + "cloudflare_custom_pages": resourceCloudflareCustomPages(), "cloudflare_filter": resourceCloudflareFilter(), "cloudflare_firewall_rule": resourceCloudflareFirewallRule(), "cloudflare_load_balancer_monitor": resourceCloudflareLoadBalancerMonitor(), diff --git a/cloudflare/resource_cloudflare_custom_pages.go b/cloudflare/resource_cloudflare_custom_pages.go new file mode 100644 index 0000000000..848f273ac6 --- /dev/null +++ b/cloudflare/resource_cloudflare_custom_pages.go @@ -0,0 +1,175 @@ +package cloudflare + +import ( + "fmt" + "log" + "strings" + + cloudflare "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "github.com/pkg/errors" +) + +func resourceCloudflareCustomPages() *schema.Resource { + return &schema.Resource{ + // Pointing the `Create` at the `Update` method here is intentional. + // Custom pages don't really get "created" as they are always + // present in Cloudflare. We just update and toggle the settings to + // be customised. + Create: resourceCloudflareCustomPagesUpdate, + Read: resourceCloudflareCustomPagesRead, + Update: resourceCloudflareCustomPagesUpdate, + Delete: resourceCloudflareCustomPagesDelete, + Importer: &schema.ResourceImporter{ + State: resourceCloudflareCustomPagesImport, + }, + + Schema: map[string]*schema.Schema{ + "zone_id": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"account_id"}, + }, + "account_id": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"zone_id"}, + }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "basic_challenge", + "waf_challenge", + "waf_block", + "ratelimit_block", + "country_challenge", + "ip_block", + "under_attack", + "500_errors", + "1000_errors", + "always_online", + }, true), + }, + "url": { + Type: schema.TypeString, + Required: true, + }, + "state": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"default", "customized"}, true), + }, + }, + } +} + +func resourceCloudflareCustomPagesRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + zoneID := d.Get("zone_id").(string) + accountID := d.Get("account_id").(string) + pageType := d.Get("type").(string) + + if accountID == "" && zoneID == "" { + return fmt.Errorf("either `account_id` or `zone_id` must be set") + } + + var pageOptions cloudflare.CustomPageOptions + + if accountID != "" { + pageOptions = cloudflare.CustomPageOptions{AccountID: accountID} + } else { + pageOptions = cloudflare.CustomPageOptions{ZoneID: zoneID} + } + + page, err := client.CustomPage(&pageOptions, pageType) + if err != nil { + return errors.New(err.Error()) + } + + // If the `page.State` comes back as "default", it's safe to assume we + // don't need to keep the ID managed anymore as it will be relying on + // Cloudflare's default pages. + if page.State == "default" { + log.Printf("[INFO] removing custom page configuration for '%s' as it is marked as being in the default state", pageType) + d.SetId("") + return nil + } + + d.Set("state", page.State) + d.Set("url", page.URL) + d.Set("type", page.ID) + + return nil +} + +func resourceCloudflareCustomPagesUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + accountID := d.Get("account_id").(string) + zoneID := d.Get("zone_id").(string) + + var pageOptions cloudflare.CustomPageOptions + if accountID != "" { + pageOptions = cloudflare.CustomPageOptions{AccountID: accountID} + } else { + pageOptions = cloudflare.CustomPageOptions{ZoneID: zoneID} + } + + pageType := d.Get("type").(string) + customPageParameters := cloudflare.CustomPageParameters{ + URL: d.Get("url").(*string), + State: "customized", + } + _, err := client.UpdateCustomPage(&pageOptions, pageType, customPageParameters) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to update '%s' custom page", pageType)) + } + + return resourceCloudflareCustomPagesRead(d, meta) +} + +func resourceCloudflareCustomPagesDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + accountID := d.Get("account_id").(string) + zoneID := d.Get("zoneID").(string) + + var pageOptions cloudflare.CustomPageOptions + if accountID != "" { + pageOptions = cloudflare.CustomPageOptions{AccountID: accountID} + } else { + pageOptions = cloudflare.CustomPageOptions{ZoneID: zoneID} + } + + pageType := d.Get("type").(string) + customPageParameters := cloudflare.CustomPageParameters{ + URL: nil, + State: "default", + } + _, err := client.UpdateCustomPage(&pageOptions, pageType, customPageParameters) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to update '%s' custom page", pageType)) + } + + return resourceCloudflareCustomPagesRead(d, meta) +} + +func resourceCloudflareCustomPagesImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + attributes := strings.SplitN(d.Id(), "/", 3) + if len(attributes) != 3 { + return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"requestType/ID/pageType\"", d.Id()) + } + requestType, identifier, pageType := attributes[0], attributes[1], attributes[2] + + d.Set("type", pageType) + + if requestType == "account" { + d.Set("account_id", identifier) + } else { + d.Set("zone_id", identifier) + } + + resourceCloudflareCustomPagesRead(d, meta) + + return []*schema.ResourceData{d}, nil +} diff --git a/cloudflare/validators.go b/cloudflare/validators.go index 7984df225d..2634fe33c4 100644 --- a/cloudflare/validators.go +++ b/cloudflare/validators.go @@ -3,6 +3,7 @@ package cloudflare import ( "fmt" "net" + "net/url" "strings" "github.com/hashicorp/terraform/helper/schema" @@ -86,3 +87,14 @@ func validateIntInSlice(valid []int) schema.SchemaValidateFunc { return nil, es } } + +// validateURL provides a method to test whether the provided string +// is a valid URL. Relying on `url.ParseRequestURI` isn't the most +// robust solution it will catch majority of the issues we're looking to +// handle here but there _could_ be edge cases. +func validateURL(v interface{}, k string) (s []string, errors []error) { + if _, err := url.ParseRequestURI(v.(string)); err != nil { + errors = append(errors, fmt.Errorf("%q: %s", k, err)) + } + return +} diff --git a/website/cloudflare.erb b/website/cloudflare.erb index 3424a316dc..566481743e 100644 --- a/website/cloudflare.erb +++ b/website/cloudflare.erb @@ -25,6 +25,9 @@ > cloudflare_access_rule + > + cloudflare_custom_pages + > cloudflare_filter diff --git a/website/docs/r/custom_pages.html.markdown b/website/docs/r/custom_pages.html.markdown new file mode 100644 index 0000000000..311e4a36bd --- /dev/null +++ b/website/docs/r/custom_pages.html.markdown @@ -0,0 +1,60 @@ +--- +layout: "cloudflare" +page_title: "Cloudflare: cloudflare_custom_pages" +sidebar_current: "docs-cloudflare-resource-custom-pages" +description: |- + Provides a resource which manages Cloudflare custom pages. +--- + +# cloudflare_custom_pages + +Provides a resource which manages Cloudflare custom error pages. + +## Example Usage + +```hcl +resource "cloudflare_custom_pages" "basic_challenge" { + zone_id = "d41d8cd98f00b204e9800998ecf8427e" + type = "basic_challenge" + url = "https://example.com/challenge.html" + state = "customized" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `zone_id` - (Optional) The zone ID where the custom pages should be + updated. Either `zone_id` or `account_id` must be provided. +* `account_id` - (Optional) The account ID where the custom pages should be + updated. Either `account_id` or `zone_id` must be provided. If + `account_id` is present, it will override the zone setting. +* `type` - (Required) The type of custom page you wish to update. Must + be one of `basic_challenge`, `waf_challenge`, `waf_block`, + `ratelimit_block`, `country_challenge`, `ip_block`, `under_attack`, + `500_errors`, `1000_errors`, `always_online`. +* `url` - (Required) URL of where the custom page source is located. +* `state` - (Required) Managed state of the custom page. Must be one of + `default`, `customised`. If the value is `default` it will be removed + from the Terraform state management. + +## Import + +Custom pages can be imported using a composite ID formed of: + +* `customPageLevel` - Either `account` or `zone`. +* `identifier` - The ID of the account or zone you intend to manage. +* `pageType` - The value from the `type` argument. + +Example for a zone: + +``` +$ terraform import cloudflare_custom_pages.basic_challenge zone/d41d8cd98f00b204e9800998ecf8427e/basic_challenge +``` + +Example for an account: + +``` +$ terraform import cloudflare_custom_pages.basic_challenge account/e268443e43d93dab7ebef303bbe9642f/basic_challenge +```