From b31d1feb51154fc2d54e064a8b46fcf287560f47 Mon Sep 17 00:00:00 2001 From: Edward Wilde Date: Sun, 18 Nov 2018 12:23:41 +0000 Subject: [PATCH] WIP Signed-off-by: Edward Wilde --- .../resource_cloudflare_spectrum_app.go | 352 ++++++++++++++++++ website/cloudflare.erb | 3 + website/docs/r/spectrum_app.html.markdown | 54 +++ 3 files changed, 409 insertions(+) create mode 100644 cloudflare/resource_cloudflare_spectrum_app.go create mode 100644 website/docs/r/spectrum_app.html.markdown diff --git a/cloudflare/resource_cloudflare_spectrum_app.go b/cloudflare/resource_cloudflare_spectrum_app.go new file mode 100644 index 00000000000..9b056060def --- /dev/null +++ b/cloudflare/resource_cloudflare_spectrum_app.go @@ -0,0 +1,352 @@ +package cloudflare + +import ( + "fmt" + "log" + "strings" + + "time" + + "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "github.com/pkg/errors" +) + +func resourceCloudflareSpectrumApp() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudflareSpectrumAppCreate, + Read: resourceCloudflareSpectrumAppRead, + Update: resourceCloudflareSpectrumAppUpdate, + Delete: resourceCloudflareSpectrumAppDelete, + Importer: &schema.ResourceImporter{ + State: resourceCloudflareSpectrumAppImport, + }, + + SchemaVersion: 0, + Schema: map[string]*schema.Schema{ + "protocol": { + Type: schema.TypeString, + Required: true, + }, + + "dns": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + + "origin_direct": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + + + "origin_dns": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + + "tls": { + Type: schema.TypeString, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{ + cloudflare., + ecs.ScopeTask, + }, false), + }, + + "created_on": { + Type: schema.TypeString, + Computed: true, + }, + + "modified_on": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +var popPoolElem = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "pop": { + Type: schema.TypeString, + Required: true, + // let the api handle validating pops + }, + + "pool_ids": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(1, 32), + }, + }, + }, +} + +var regionPoolElem = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Required: true, + // let the api handle validating regions + }, + + "pool_ids": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(1, 32), + }, + }, + }, +} + +var localPoolElems = map[string]*schema.Resource{ + "pop": popPoolElem, + "region": regionPoolElem, +} + +func resourceCloudflareSpectrumAppCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + + newSpectrumApp := cloudflare.SpectrumApp{ + Name: d.Get("name").(string), + FallbackPool: d.Get("fallback_pool_id").(string), + DefaultPools: expandInterfaceToStringList(d.Get("default_pool_ids")), + Proxied: d.Get("proxied").(bool), + TTL: d.Get("ttl").(int), + SteeringPolicy: d.Get("steering_policy").(string), + Persistence: d.Get("session_affinity").(string), + } + + if description, ok := d.GetOk("description"); ok { + newSpectrumApp.Description = description.(string) + } + + if ttl, ok := d.GetOk("ttl"); ok { + newSpectrumApp.TTL = ttl.(int) + } + + if regionPools, ok := d.GetOk("region_pools"); ok { + expandedRegionPools, err := expandGeoPools(regionPools, "region") + if err != nil { + return err + } + newSpectrumApp.RegionPools = expandedRegionPools + } + + if popPools, ok := d.GetOk("pop_pools"); ok { + expandedPopPools, err := expandGeoPools(popPools, "pop") + if err != nil { + return err + } + newSpectrumApp.PopPools = expandedPopPools + } + + zoneName := d.Get("zone").(string) + zoneID, err := client.ZoneIDByName(zoneName) + if err != nil { + return fmt.Errorf("error finding zone %q: %s", zoneName, err) + } + d.Set("zone_id", zoneID) + + log.Printf("[INFO] Creating Cloudflare Load Balancer from struct: %+v", newSpectrumApp) + + r, err := client.CreateSpectrumApp(zoneID, newSpectrumApp) + if err != nil { + return errors.Wrap(err, "error creating load balancer for zone") + } + + if r.ID == "" { + return fmt.Errorf("failed to find id in Create response; resource was empty") + } + + d.SetId(r.ID) + + log.Printf("[INFO] Cloudflare Load Balancer ID: %s", d.Id()) + + return resourceCloudflareSpectrumAppRead(d, meta) +} + +func resourceCloudflareSpectrumAppUpdate(d *schema.ResourceData, meta interface{}) error { + // since api only supports replace, update looks a lot like create... + client := meta.(*cloudflare.API) + zoneID := d.Get("zone_id").(string) + + loadBalancer := cloudflare.SpectrumApp{ + ID: d.Id(), + Name: d.Get("name").(string), + FallbackPool: d.Get("fallback_pool_id").(string), + DefaultPools: expandInterfaceToStringList(d.Get("default_pool_ids")), + Proxied: d.Get("proxied").(bool), + TTL: d.Get("ttl").(int), + SteeringPolicy: d.Get("steering_policy").(string), + Persistence: d.Get("session_affinity").(string), + } + + if description, ok := d.GetOk("description"); ok { + loadBalancer.Description = description.(string) + } + + if regionPools, ok := d.GetOk("region_pools"); ok { + expandedRegionPools, err := expandGeoPools(regionPools, "region") + if err != nil { + return err + } + loadBalancer.RegionPools = expandedRegionPools + } + + if popPools, ok := d.GetOk("pop_pools"); ok { + expandedPopPools, err := expandGeoPools(popPools, "pop") + if err != nil { + return err + } + loadBalancer.PopPools = expandedPopPools + } + + log.Printf("[INFO] Updating Cloudflare Load Balancer from struct: %+v", loadBalancer) + + _, err := client.ModifySpectrumApp(zoneID, loadBalancer) + if err != nil { + return errors.Wrap(err, "error creating load balancer for zone") + } + + return resourceCloudflareSpectrumAppRead(d, meta) +} + +func expandGeoPools(pool interface{}, geoType string) (map[string][]string, error) { + cfg := pool.(*schema.Set).List() + expanded := make(map[string][]string) + for _, v := range cfg { + locationConfig := v.(map[string]interface{}) + // lists are of type interface{} by default + location := locationConfig[geoType].(string) + if _, present := expanded[location]; !present { + expanded[location] = expandInterfaceToStringList(locationConfig["pool_ids"]) + } else { + return nil, fmt.Errorf("duplicate entry specified for %s pool in location %q. each location must only be specified once", geoType, location) + } + } + return expanded, nil +} + +func resourceCloudflareSpectrumAppRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + zoneID := d.Get("zone_id").(string) + loadBalancerID := d.Id() + + loadBalancer, err := client.SpectrumAppDetails(zoneID, loadBalancerID) + if err != nil { + if strings.Contains(err.Error(), "HTTP status 404") { + log.Printf("[INFO] Load balancer %s in zone %s not found", loadBalancerID, zoneID) + d.SetId("") + return nil + } + return errors.Wrap(err, + fmt.Sprintf("Error reading load balancer resource from API for resource %s in zone %s", zoneID, loadBalancerID)) + } + + d.Set("name", loadBalancer.Name) + d.Set("fallback_pool_id", loadBalancer.FallbackPool) + d.Set("proxied", loadBalancer.Proxied) + d.Set("description", loadBalancer.Description) + d.Set("ttl", loadBalancer.TTL) + d.Set("steering_policy", loadBalancer.SteeringPolicy) + d.Set("created_on", loadBalancer.CreatedOn.Format(time.RFC3339Nano)) + d.Set("modified_on", loadBalancer.ModifiedOn.Format(time.RFC3339Nano)) + + if err := d.Set("default_pool_ids", loadBalancer.DefaultPools); err != nil { + log.Printf("[WARN] Error setting default_pool_ids on load balancer %q: %s", d.Id(), err) + } + + if err := d.Set("pop_pools", flattenGeoPools(loadBalancer.PopPools, "pop")); err != nil { + log.Printf("[WARN] Error setting pop_pools on load balancer %q: %s", d.Id(), err) + } + + if err := d.Set("region_pools", flattenGeoPools(loadBalancer.RegionPools, "region")); err != nil { + log.Printf("[WARN] Error setting region_pools on load balancer %q: %s", d.Id(), err) + } + + return nil +} + +func flattenGeoPools(pools map[string][]string, geoType string) *schema.Set { + flattened := make([]interface{}, 0) + for k, v := range pools { + geoConf := map[string]interface{}{ + geoType: k, + "pool_ids": flattenStringList(v), + } + flattened = append(flattened, geoConf) + } + return schema.NewSet(schema.HashResource(localPoolElems[geoType]), flattened) +} + +func resourceCloudflareSpectrumAppDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + zoneID := d.Get("zone_id").(string) + loadBalancerID := d.Id() + + log.Printf("[INFO] Deleting Cloudflare Load Balancer: %s in zone: %s", loadBalancerID, zoneID) + + err := client.DeleteSpectrumApp(zoneID, loadBalancerID) + if err != nil { + return fmt.Errorf("error deleting Cloudflare Load Balancer: %s", err) + } + + return nil +} + +func resourceCloudflareSpectrumAppImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + client := meta.(*cloudflare.API) + + // split the id so we can lookup + idAttr := strings.SplitN(d.Id(), "/", 2) + var zoneName string + var loadBalancerID string + if len(idAttr) == 2 { + zoneName = idAttr[0] + loadBalancerID = idAttr[1] + } else { + return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"zoneName/loadBalancerID\"", d.Id()) + } + zoneID, err := client.ZoneIDByName(zoneName) + + if err != nil { + return nil, fmt.Errorf("error finding zoneName %q: %s", zoneName, err) + } + + d.Set("zone", zoneName) + d.Set("zone_id", zoneID) + d.SetId(loadBalancerID) + return []*schema.ResourceData{d}, nil +} diff --git a/website/cloudflare.erb b/website/cloudflare.erb index c73ee4ce032..0ff8e09f406 100644 --- a/website/cloudflare.erb +++ b/website/cloudflare.erb @@ -61,6 +61,9 @@ > cloudflare_record + > + cloudflare_spectrum_app + > cloudflare_waf_rule diff --git a/website/docs/r/spectrum_app.html.markdown b/website/docs/r/spectrum_app.html.markdown new file mode 100644 index 00000000000..9de43fc4b23 --- /dev/null +++ b/website/docs/r/spectrum_app.html.markdown @@ -0,0 +1,54 @@ +--- +layout: "cloudflare" +page_title: "Cloudflare: cloudflare_spectrum_app" +sidebar_current: "docs-cloudflare-resource-load-balancer" +description: |- + Provides a Cloudflare Sprectrum Application resource. +--- + +# cloudflare_spectrum_app + +## Example Usage + +```hcl +# Define a spectrum application proxies ssh traffic +resource "cloudflare_spectrum_app" "ssh_proxy" { + protocol = "tcp/22" + dns = { + type = "CNAME" + name = "ssh.example.com" + } + + origin_direct = [ + "tcp://109.151.40.129:22" + ] +} +``` + +Provides a Cloudflare Load Balancer resource. You can extend the power of Cloudflare's DDoS, TLS, and IP Firewall to your other TCP-based services. This allows you to proxy tcp traffic over the Cloudflare network. + +## Argument Reference +* `protocol` - (Required) The port configuration at Cloudflare’s edge. i.e. `tcp/22` +* `dns` - (Required) The name and type of DNS record for the Spectrum application. Fields documented below. +* `origin_direct` (Optional) A list of destination addresses to the origin. i.e. `tcp://192.0.2.1:22` +* `origin_dns` (Optional) A destination dns addresses to the origin. Fields documented below +* `origin_port` (Optional) If using `origin_dns` this is a required attribute. Origin port to proxy traffice to i.e. `22` +* `tls` (Optional) If `On` Cloudflare will decrypt traffic for your application at the edge. Defaults to `Off` +* `ip_firewall` (Optional) Enables the IP Firewall for this application. Defaults to `true` +* `proxy_protocol` (Optional) Enables Proxy Protocol v1 to the origin. Defaults to `false` + +**dns** + +* `type` (Requried) The type of DNS record associated with the application. Valid values: `CNAME` +* `name` (Required) The name of the DNS record associated with the application.i.e. `ssh.example.com` + +**origin_dns** + +* `name` - (Required) Fully qualified domain name of the origin i.e. origin-ssh.example.com +## Attributes Reference + +The following attributes are exported: + +* `id` - Unique identifier in the API for the spectrum application. +* `created_on` - The RFC3339 timestamp of when the spectrum application was created. +* `modified_on` - The RFC3339 timestamp of when the spectrum application was last modified. \ No newline at end of file