From 2ad37bba4a65a18891586389919acb1006e304d6 Mon Sep 17 00:00:00 2001 From: clint shryock Date: Wed, 23 Mar 2016 14:53:50 -0500 Subject: [PATCH] provider/fastly: Add Fastly Provider, ServiceV1 resource --- builtin/bins/provider-fastly/main.go | 12 + builtin/bins/provider-fastly/main_test.go | 1 + builtin/providers/fastly/config.go | 31 + builtin/providers/fastly/provider.go | 34 + builtin/providers/fastly/provider_test.go | 35 + .../fastly/resource_fastly_service_v1.go | 603 ++++++++++++++++++ .../fastly/resource_fastly_service_v1_test.go | 409 ++++++++++++ website/source/assets/stylesheets/_docs.scss | 1 + .../docs/providers/fastly/index.html.markdown | 80 +++ .../fastly/r/service_v1.html.markdown | 136 ++++ website/source/layouts/docs.erb | 4 + website/source/layouts/fastly.erb | 28 + 12 files changed, 1374 insertions(+) create mode 100644 builtin/bins/provider-fastly/main.go create mode 100644 builtin/bins/provider-fastly/main_test.go create mode 100644 builtin/providers/fastly/config.go create mode 100644 builtin/providers/fastly/provider.go create mode 100644 builtin/providers/fastly/provider_test.go create mode 100644 builtin/providers/fastly/resource_fastly_service_v1.go create mode 100644 builtin/providers/fastly/resource_fastly_service_v1_test.go create mode 100644 website/source/docs/providers/fastly/index.html.markdown create mode 100644 website/source/docs/providers/fastly/r/service_v1.html.markdown create mode 100644 website/source/layouts/fastly.erb diff --git a/builtin/bins/provider-fastly/main.go b/builtin/bins/provider-fastly/main.go new file mode 100644 index 000000000..f9cfbc978 --- /dev/null +++ b/builtin/bins/provider-fastly/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/fastly" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: fastly.Provider, + }) +} diff --git a/builtin/bins/provider-fastly/main_test.go b/builtin/bins/provider-fastly/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-fastly/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/fastly/config.go b/builtin/providers/fastly/config.go new file mode 100644 index 000000000..a2e194818 --- /dev/null +++ b/builtin/providers/fastly/config.go @@ -0,0 +1,31 @@ +package fastly + +import ( + "fmt" + + gofastly "github.com/sethvargo/go-fastly" +) + +type Config struct { + ApiKey string +} + +type FastlyClient struct { + conn *gofastly.Client +} + +func (c *Config) Client() (interface{}, error) { + var client FastlyClient + + if c.ApiKey == "" { + return nil, fmt.Errorf("[Err] No API key for Fastly") + } + + fconn, err := gofastly.NewClient(c.ApiKey) + if err != nil { + return nil, err + } + + client.conn = fconn + return &client, nil +} diff --git a/builtin/providers/fastly/provider.go b/builtin/providers/fastly/provider.go new file mode 100644 index 000000000..f68c6705b --- /dev/null +++ b/builtin/providers/fastly/provider.go @@ -0,0 +1,34 @@ +package fastly + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +// Provider returns a terraform.ResourceProvider. +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "api_key": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "FASTLY_API_KEY", + }, nil), + Description: "Fastly API Key from https://app.fastly.com/#account", + }, + }, + ResourcesMap: map[string]*schema.Resource{ + "fastly_service_v1": resourceServiceV1(), + }, + + ConfigureFunc: providerConfigure, + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := Config{ + ApiKey: d.Get("api_key").(string), + } + return config.Client() +} diff --git a/builtin/providers/fastly/provider_test.go b/builtin/providers/fastly/provider_test.go new file mode 100644 index 000000000..e567354b0 --- /dev/null +++ b/builtin/providers/fastly/provider_test.go @@ -0,0 +1,35 @@ +package fastly + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "fastly": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("FASTLY_API_KEY"); v == "" { + t.Fatal("FASTLY_API_KEY must be set for acceptance tests") + } +} diff --git a/builtin/providers/fastly/resource_fastly_service_v1.go b/builtin/providers/fastly/resource_fastly_service_v1.go new file mode 100644 index 000000000..3226ec1bf --- /dev/null +++ b/builtin/providers/fastly/resource_fastly_service_v1.go @@ -0,0 +1,603 @@ +package fastly + +import ( + "errors" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/schema" + gofastly "github.com/sethvargo/go-fastly" +) + +var fastlyNoServiceFoundErr = errors.New("No matching Fastly Service found") + +func resourceServiceV1() *schema.Resource { + return &schema.Resource{ + Create: resourceServiceV1Create, + Read: resourceServiceV1Read, + Update: resourceServiceV1Update, + Delete: resourceServiceV1Delete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "Unique name for this Service", + }, + + // Active Version represents the currently activated version in Fastly. In + // Terraform, we abstract this number away from the users and manage + // creating and activating. It's used internally, but also exported for + // users to see. + "active_version": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "domain": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "The domain that this Service will respond to", + }, + + "comment": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "default_ttl": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 3600, + Description: "The default Time-to-live (TTL) for the version", + }, + + "default_host": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The default hostname for the version", + }, + + "backend": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // required fields + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "A name for this Backend", + }, + "address": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "An IPv4, hostname, or IPv6 address for the Backend", + }, + // Optional fields, defaults where they exist + "auto_loadbalance": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Should this Backend be load balanced", + }, + "between_bytes_timeout": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 10000, + Description: "How long to wait between bytes in milliseconds", + }, + "connect_timeout": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 1000, + Description: "How long to wait for a timeout in milliseconds", + }, + "error_threshold": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: "Number of errors to allow before the Backend is marked as down", + }, + "first_byte_timeout": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 15000, + Description: "How long to wait for the first bytes in milliseconds", + }, + "max_conn": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 200, + Description: "Maximum number of connections for this Backend", + }, + "port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 80, + Description: "The port number Backend responds on. Default 80", + }, + "ssl_check_cert": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Be strict on checking SSL certs", + }, + // UseSSL is something we want to support in the future, but + // requires SSL setup we don't yet have + // TODO: Provide all SSL fields from https://docs.fastly.com/api/config#backend + // "use_ssl": &schema.Schema{ + // Type: schema.TypeBool, + // Optional: true, + // Default: false, + // Description: "Whether or not to use SSL to reach the Backend", + // }, + "weight": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 100, + Description: "How long to wait for the first bytes in milliseconds", + }, + }, + }, + }, + + "force_destroy": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + }, + } +} + +func resourceServiceV1Create(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*FastlyClient).conn + service, err := conn.CreateService(&gofastly.CreateServiceInput{ + Name: d.Get("name").(string), + Comment: "Managed by Terraform", + }) + + if err != nil { + return err + } + + d.SetId(service.ID) + return resourceServiceV1Update(d, meta) +} + +func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*FastlyClient).conn + + // Update Name. No new verions is required for this + if d.HasChange("name") { + _, err := conn.UpdateService(&gofastly.UpdateServiceInput{ + ID: d.Id(), + Name: d.Get("name").(string), + }) + if err != nil { + return err + } + } + + // Once activated, Versions are locked and become immutable. This is true for + // versions that are no longer active. For Domains, Backends, DefaultHost and + // DefaultTTL, a new Version must be created first, and updates posted to that + // Version. Loop these attributes and determine if we need to create a new version first + var needsChange bool + for _, v := range []string{"domain", "backend", "default_host", "default_ttl"} { + if d.HasChange(v) { + needsChange = true + } + } + + if needsChange { + latestVersion := d.Get("active_version").(string) + if latestVersion == "" { + // If the service was just created, there is an empty Version 1 available + // that is unlocked and can be updated + latestVersion = "1" + } else { + // Clone the latest version, giving us an unlocked version we can modify + log.Printf("[DEBUG] Creating clone of version (%s) for updates", latestVersion) + newVersion, err := conn.CloneVersion(&gofastly.CloneVersionInput{ + Service: d.Id(), + Version: latestVersion, + }) + if err != nil { + return err + } + + // The new version number is named "Number", but it's actually a string + latestVersion = newVersion.Number + + // New versions are not immediately found in the API, or are not + // immediately mutable, so we need to sleep a few and let Fastly ready + // itself. Typically, 7 seconds is enough + time.Sleep(7 * time.Second) + } + + // update general settings + if d.HasChange("default_host") || d.HasChange("default_ttl") { + opts := gofastly.UpdateSettingsInput{ + Service: d.Id(), + Version: latestVersion, + // default_ttl has the same default value of 3600 that is provided by + // the Fastly API, so it's safe to include here + DefaultTTL: uint(d.Get("default_ttl").(int)), + } + + if attr, ok := d.GetOk("default_host"); ok { + opts.DefaultHost = attr.(string) + } + + log.Printf("[DEBUG] Update Settings opts: %#v", opts) + _, err := conn.UpdateSettings(&opts) + if err != nil { + return err + } + } + + // Find differences in domains + if d.HasChange("domain") { + // Note: we don't utilize the PUT endpoint to update a Domain, we simply + // destroy it and create a new one. This is how Terraform works with nested + // sub resources, we only get the full diff not a partial set item diff. + // Because this is done on a new version of the configuration, this is + // considered safe + od, nd := d.GetChange("domain") + if od == nil { + od = new(schema.Set) + } + if nd == nil { + nd = new(schema.Set) + } + + ods := od.(*schema.Set) + nds := nd.(*schema.Set) + + remove := ods.Difference(nds).List() + add := nds.Difference(ods).List() + + // Delete removed domains + for _, dRaw := range remove { + df := dRaw.(map[string]interface{}) + opts := gofastly.DeleteDomainInput{ + Service: d.Id(), + Version: latestVersion, + Name: df["name"].(string), + } + + log.Printf("[DEBUG] Fastly Domain Removal opts: %#v", opts) + err := conn.DeleteDomain(&opts) + if err != nil { + return err + } + } + + // POST new Domains + for _, dRaw := range add { + df := dRaw.(map[string]interface{}) + opts := gofastly.CreateDomainInput{ + Service: d.Id(), + Version: latestVersion, + Name: df["name"].(string), + } + + if v, ok := df["comment"]; ok { + opts.Comment = v.(string) + } + + log.Printf("[DEBUG] Fastly Domain Addition opts: %#v", opts) + _, err := conn.CreateDomain(&opts) + if err != nil { + return err + } + } + } + + // find difference in backends + if d.HasChange("backend") { + // POST new Backends + // Note: we don't utilize the PUT endpoint to update a Backend, we simply + // destroy it and create a new one. This is how Terraform works with nested + // sub resources, we only get the full diff not a partial set item diff. + // Because this is done on a new version of the configuration, this is + // considered safe + ob, nb := d.GetChange("backend") + if ob == nil { + ob = new(schema.Set) + } + if nb == nil { + nb = new(schema.Set) + } + + obs := ob.(*schema.Set) + nbs := nb.(*schema.Set) + removeBackends := obs.Difference(nbs).List() + addBackends := nbs.Difference(obs).List() + + // DELETE old Backends + for _, bRaw := range removeBackends { + bf := bRaw.(map[string]interface{}) + opts := gofastly.DeleteBackendInput{ + Service: d.Id(), + Version: latestVersion, + Name: bf["name"].(string), + } + + log.Printf("[DEBUG] Fastly Backend Removal opts: %#v", opts) + err := conn.DeleteBackend(&opts) + if err != nil { + return err + } + } + + for _, dRaw := range addBackends { + df := dRaw.(map[string]interface{}) + opts := gofastly.CreateBackendInput{ + Service: d.Id(), + Version: latestVersion, + Name: df["name"].(string), + Address: df["address"].(string), + AutoLoadbalance: df["auto_loadbalance"].(bool), + SSLCheckCert: df["ssl_check_cert"].(bool), + Port: uint(df["port"].(int)), + BetweenBytesTimeout: uint(df["between_bytes_timeout"].(int)), + ConnectTimeout: uint(df["connect_timeout"].(int)), + ErrorThreshold: uint(df["error_threshold"].(int)), + FirstByteTimeout: uint(df["first_byte_timeout"].(int)), + MaxConn: uint(df["max_conn"].(int)), + Weight: uint(df["weight"].(int)), + } + + log.Printf("[DEBUG] Create Backend Opts: %#v", opts) + _, err := conn.CreateBackend(&opts) + if err != nil { + return err + } + } + } + + // validate version + log.Printf("[DEBUG] Validating Fastly Service (%s), Version (%s)", d.Id(), latestVersion) + valid, msg, err := conn.ValidateVersion(&gofastly.ValidateVersionInput{ + Service: d.Id(), + Version: latestVersion, + }) + + if err != nil { + return fmt.Errorf("[ERR] Error checking validation: %s", err) + } + + if !valid { + return fmt.Errorf("[WARN] Invalid configuration for Fastly Service (%s): %s", d.Id(), msg) + } + + log.Printf("[DEBUG] Activating Fastly Service (%s), Version (%s)", d.Id(), latestVersion) + _, err = conn.ActivateVersion(&gofastly.ActivateVersionInput{ + Service: d.Id(), + Version: latestVersion, + }) + if err != nil { + return fmt.Errorf("[ERR] Error activating version (%s): %s", latestVersion, err) + } + + // Only if the version is valid and activated do we set the active_version. + // This prevents us from getting stuck in cloning an invalid version + d.Set("active_version", latestVersion) + } + + return resourceServiceV1Read(d, meta) +} + +func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*FastlyClient).conn + + // Find the Service. Discard the service because we need the ServiceDetails, + // not just a Service record + _, err := findService(d.Id(), meta) + if err != nil { + switch err { + case fastlyNoServiceFoundErr: + log.Printf("[WARN] %s for ID (%s)", err, d.Id()) + d.SetId("") + return nil + default: + return err + } + } + + s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{ + ID: d.Id(), + }) + + if err != nil { + return err + } + + d.Set("name", s.Name) + d.Set("active_version", s.ActiveVersion.Number) + + // If CreateService succeeds, but initial updates to the Service fail, we'll + // have an empty ActiveService version (no version is active, so we can't + // query for information on it) + if s.ActiveVersion.Number != "" { + settingsOpts := gofastly.GetSettingsInput{ + Service: d.Id(), + Version: s.ActiveVersion.Number, + } + if settings, err := conn.GetSettings(&settingsOpts); err == nil { + d.Set("default_host", settings.DefaultHost) + d.Set("default_ttl", settings.DefaultTTL) + } else { + return fmt.Errorf("[ERR] Error looking up Version settings for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) + } + + // TODO: update go-fastly to support an ActiveVersion struct, which contains + // domain and backend info in the response. Here we do 2 additional queries + // to find out that info + domainList, err := conn.ListDomains(&gofastly.ListDomainsInput{ + Service: d.Id(), + Version: s.ActiveVersion.Number, + }) + + if err != nil { + return fmt.Errorf("[ERR] Error looking up Domains for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) + } + + // Refresh Domains + dl := flattenDomains(domainList) + + if err := d.Set("domain", dl); err != nil { + log.Printf("[WARN] Error setting Domains for (%s): %s", d.Id(), err) + } + + // Refresh Backends + backendList, err := conn.ListBackends(&gofastly.ListBackendsInput{ + Service: d.Id(), + Version: s.ActiveVersion.Number, + }) + + if err != nil { + return fmt.Errorf("[ERR] Error looking up Backends for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) + } + + bl := flattenBackends(backendList) + + if err := d.Set("backend", bl); err != nil { + log.Printf("[WARN] Error setting Backends for (%s): %s", d.Id(), err) + } + } else { + log.Printf("[DEBUG] Active Version for Service (%s) is empty, no state to refresh", d.Id()) + } + + return nil +} + +func resourceServiceV1Delete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*FastlyClient).conn + + // Fastly will fail to delete any service with an Active Version. + // If `force_destroy` is given, we deactivate the active version and then send + // the DELETE call + if d.Get("force_destroy").(bool) { + s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{ + ID: d.Id(), + }) + + if err != nil { + return err + } + + if s.ActiveVersion.Number != "" { + _, err := conn.DeactivateVersion(&gofastly.DeactivateVersionInput{ + Service: d.Id(), + Version: s.ActiveVersion.Number, + }) + if err != nil { + return err + } + } + } + + err := conn.DeleteService(&gofastly.DeleteServiceInput{ + ID: d.Id(), + }) + + if err != nil { + return err + } + + _, err = findService(d.Id(), meta) + if err != nil { + switch err { + // we expect no records to be found here + case fastlyNoServiceFoundErr: + d.SetId("") + return nil + default: + return err + } + } + + // findService above returned something and nil error, but shouldn't have + return fmt.Errorf("[WARN] Tried deleting Service (%s), but was still found", d.Id()) + +} + +func flattenDomains(list []*gofastly.Domain) []map[string]interface{} { + dl := make([]map[string]interface{}, 0, len(list)) + + for _, d := range list { + dl = append(dl, map[string]interface{}{ + "name": d.Name, + "comment": d.Comment, + }) + } + + return dl +} + +func flattenBackends(backendList []*gofastly.Backend) []map[string]interface{} { + var bl []map[string]interface{} + for _, b := range backendList { + // Convert Backend to a map for saving to state. + nb := map[string]interface{}{ + "name": b.Name, + "address": b.Address, + "auto_loadbalance": b.AutoLoadbalance, + "between_bytes_timeout": int(b.BetweenBytesTimeout), + "connect_timeout": int(b.ConnectTimeout), + "error_threshold": int(b.ErrorThreshold), + "first_byte_timeout": int(b.FirstByteTimeout), + "max_conn": int(b.MaxConn), + "port": int(b.Port), + "ssl_check_cert": b.SSLCheckCert, + "weight": int(b.Weight), + } + + bl = append(bl, nb) + } + return bl +} + +// findService finds a Fastly Service via the ListServices endpoint, returning +// the Service if found. +// +// Fastly API does not include any "deleted_at" type parameter to indicate +// that a Service has been deleted. GET requests to a deleted Service will +// return 200 OK and have the full output of the Service for an unknown time +// (days, in my testing). In order to determine if a Service is deleted, we +// need to hit /service and loop the returned Services, searching for the one +// in question. This endpoint only returns active or "alive" services. If the +// Service is not included, then it's "gone" +// +// Returns a fastlyNoServiceFoundErr error if the Service is not found in the +// ListServices response. +func findService(id string, meta interface{}) (*gofastly.Service, error) { + conn := meta.(*FastlyClient).conn + + l, err := conn.ListServices(&gofastly.ListServicesInput{}) + if err != nil { + return nil, fmt.Errorf("[WARN] Error listing servcies when deleting Fastly Service (%s): %s", id, err) + } + + for _, s := range l { + if s.ID == id { + log.Printf("[DEBUG] Found Service (%s)", id) + return s, nil + } + } + + return nil, fastlyNoServiceFoundErr +} diff --git a/builtin/providers/fastly/resource_fastly_service_v1_test.go b/builtin/providers/fastly/resource_fastly_service_v1_test.go new file mode 100644 index 000000000..d229b8fd6 --- /dev/null +++ b/builtin/providers/fastly/resource_fastly_service_v1_test.go @@ -0,0 +1,409 @@ +package fastly + +import ( + "fmt" + "reflect" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + gofastly "github.com/sethvargo/go-fastly" +) + +func TestResourceFastlyFlattenDomains(t *testing.T) { + cases := []struct { + remote []*gofastly.Domain + local []map[string]interface{} + }{ + { + remote: []*gofastly.Domain{ + &gofastly.Domain{ + Name: "test.notexample.com", + Comment: "not comment", + }, + }, + local: []map[string]interface{}{ + map[string]interface{}{ + "name": "test.notexample.com", + "comment": "not comment", + }, + }, + }, + { + remote: []*gofastly.Domain{ + &gofastly.Domain{ + Name: "test.notexample.com", + }, + }, + local: []map[string]interface{}{ + map[string]interface{}{ + "name": "test.notexample.com", + "comment": "", + }, + }, + }, + } + + for _, c := range cases { + out := flattenDomains(c.remote) + if !reflect.DeepEqual(out, c.local) { + t.Fatalf("Error matching:\nexpected: %#v\ngot: %#v", c.local, out) + } + } +} + +func TestResourceFastlyFlattenBackend(t *testing.T) { + cases := []struct { + remote []*gofastly.Backend + local []map[string]interface{} + }{ + { + remote: []*gofastly.Backend{ + &gofastly.Backend{ + Name: "test.notexample.com", + Address: "www.notexample.com", + Port: uint(80), + AutoLoadbalance: true, + BetweenBytesTimeout: uint(10000), + ConnectTimeout: uint(1000), + ErrorThreshold: uint(0), + FirstByteTimeout: uint(15000), + MaxConn: uint(200), + SSLCheckCert: true, + Weight: uint(100), + }, + }, + local: []map[string]interface{}{ + map[string]interface{}{ + "name": "test.notexample.com", + "address": "www.notexample.com", + "port": 80, + "auto_loadbalance": true, + "between_bytes_timeout": 10000, + "connect_timeout": 1000, + "error_threshold": 0, + "first_byte_timeout": 15000, + "max_conn": 200, + "ssl_check_cert": true, + "weight": 100, + }, + }, + }, + } + + for _, c := range cases { + out := flattenBackends(c.remote) + if !reflect.DeepEqual(out, c.local) { + t.Fatalf("Error matching:\nexpected: %#v\ngot: %#v", c.local, out) + } + } +} + +func TestAccFastlyServiceV1_updateDomain(t *testing.T) { + var service gofastly.ServiceDetail + name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + nameUpdate := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + domainName1 := fmt.Sprintf("%s.notadomain.com", acctest.RandString(10)) + domainName2 := fmt.Sprintf("%s.notadomain.com", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckServiceV1Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccServiceV1Config(name, domainName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists("fastly_service_v1.foo", &service), + testAccCheckFastlyServiceV1Attributes(&service, name, []string{domainName1}), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "name", name), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "active_version", "1"), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "domain.#", "1"), + ), + }, + + resource.TestStep{ + Config: testAccServiceV1Config_domainUpdate(nameUpdate, domainName1, domainName2), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists("fastly_service_v1.foo", &service), + testAccCheckFastlyServiceV1Attributes(&service, nameUpdate, []string{domainName1, domainName2}), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "name", nameUpdate), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "active_version", "2"), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "domain.#", "2"), + ), + }, + }, + }) +} + +func TestAccFastlyServiceV1_updateBackend(t *testing.T) { + var service gofastly.ServiceDetail + name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + backendName := fmt.Sprintf("%s.aws.amazon.com", acctest.RandString(3)) + backendName2 := fmt.Sprintf("%s.aws.amazon.com", acctest.RandString(3)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckServiceV1Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccServiceV1Config_backend(name, backendName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists("fastly_service_v1.foo", &service), + testAccCheckFastlyServiceV1Attributes_backends(&service, name, []string{backendName}), + ), + }, + + resource.TestStep{ + Config: testAccServiceV1Config_backend_update(name, backendName, backendName2), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists("fastly_service_v1.foo", &service), + testAccCheckFastlyServiceV1Attributes_backends(&service, name, []string{backendName, backendName2}), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "active_version", "2"), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "backend.#", "2"), + ), + }, + }, + }) +} + +func TestAccFastlyServiceV1_basic(t *testing.T) { + var service gofastly.ServiceDetail + name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + domainName := fmt.Sprintf("%s.notadomain.com", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckServiceV1Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccServiceV1Config(name, domainName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists("fastly_service_v1.foo", &service), + testAccCheckFastlyServiceV1Attributes(&service, name, []string{domainName}), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "name", name), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "active_version", "1"), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "domain.#", "1"), + ), + }, + }, + }) +} + +func testAccCheckServiceV1Exists(n string, service *gofastly.ServiceDetail) 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 Service ID is set") + } + + conn := testAccProvider.Meta().(*FastlyClient).conn + latest, err := conn.GetServiceDetails(&gofastly.GetServiceInput{ + ID: rs.Primary.ID, + }) + + if err != nil { + return err + } + + *service = *latest + + return nil + } +} + +func testAccCheckFastlyServiceV1Attributes(service *gofastly.ServiceDetail, name string, domains []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if service.Name != name { + return fmt.Errorf("Bad name, expected (%s), got (%s)", name, service.Name) + } + + conn := testAccProvider.Meta().(*FastlyClient).conn + domainList, err := conn.ListDomains(&gofastly.ListDomainsInput{ + Service: service.ID, + Version: service.ActiveVersion.Number, + }) + + if err != nil { + return fmt.Errorf("[ERR] Error looking up Domains for (%s), version (%s): %s", service.Name, service.ActiveVersion.Number, err) + } + + expected := len(domains) + for _, d := range domainList { + for _, e := range domains { + if d.Name == e { + expected-- + } + } + } + + if expected > 0 { + return fmt.Errorf("Domain count mismatch, expected: %#v, got: %#v", domains, domainList) + } + + return nil + } +} + +func testAccCheckFastlyServiceV1Attributes_backends(service *gofastly.ServiceDetail, name string, backends []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if service.Name != name { + return fmt.Errorf("Bad name, expected (%s), got (%s)", name, service.Name) + } + + conn := testAccProvider.Meta().(*FastlyClient).conn + backendList, err := conn.ListBackends(&gofastly.ListBackendsInput{ + Service: service.ID, + Version: service.ActiveVersion.Number, + }) + + if err != nil { + return fmt.Errorf("[ERR] Error looking up Backends for (%s), version (%s): %s", service.Name, service.ActiveVersion.Number, err) + } + + expected := len(backendList) + for _, b := range backendList { + for _, e := range backends { + if b.Address == e { + expected-- + } + } + } + + if expected > 0 { + return fmt.Errorf("Backend count mismatch, expected: %#v, got: %#v", backends, backendList) + } + + return nil + } +} + +func testAccCheckServiceV1Destroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "fastly_service_v1" { + continue + } + + conn := testAccProvider.Meta().(*FastlyClient).conn + l, err := conn.ListServices(&gofastly.ListServicesInput{}) + if err != nil { + return fmt.Errorf("[WARN] Error listing servcies when deleting Fastly Service (%s): %s", rs.Primary.ID, err) + } + + for _, s := range l { + if s.ID == rs.Primary.ID { + // service still found + return fmt.Errorf("[WARN] Tried deleting Service (%s), but was still found", rs.Primary.ID) + } + } + } + return nil +} + +func testAccServiceV1Config(name, domain string) string { + return fmt.Sprintf(` +resource "fastly_service_v1" "foo" { + name = "%s" + + domain { + name = "%s" + comment = "tf-testing-domain" + } + + backend { + address = "aws.amazon.com" + name = "amazon docs" + } + + force_destroy = true +}`, name, domain) +} + +func testAccServiceV1Config_domainUpdate(name, domain1, domain2 string) string { + return fmt.Sprintf(` +resource "fastly_service_v1" "foo" { + name = "%s" + + domain { + name = "%s" + comment = "tf-testing-domain" + } + + domain { + name = "%s" + comment = "tf-testing-other-domain" + } + + backend { + address = "aws.amazon.com" + name = "amazon docs" + } + + force_destroy = true +}`, name, domain1, domain2) +} + +func testAccServiceV1Config_backend(name, backend string) string { + return fmt.Sprintf(` +resource "fastly_service_v1" "foo" { + name = "%s" + + domain { + name = "test.notadomain.com" + comment = "tf-testing-domain" + } + + backend { + address = "%s" + name = "tf -test backend" + } + + force_destroy = true +}`, name, backend) +} + +func testAccServiceV1Config_backend_update(name, backend, backend2 string) string { + return fmt.Sprintf(` +resource "fastly_service_v1" "foo" { + name = "%s" + + default_ttl = 3400 + + domain { + name = "test.notadomain.com" + comment = "tf-testing-domain" + } + + backend { + address = "%s" + name = "tf-test-backend" + } + + backend { + address = "%s" + name = "tf-test-backend-other" + } + + force_destroy = true +}`, name, backend, backend2) +} diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index 50461047d..2fdeba66d 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -22,6 +22,7 @@ body.layout-dnsimple, body.layout-docker, body.layout-dyn, body.layout-github, +body.layout-fastly, body.layout-google, body.layout-heroku, body.layout-influxdb, diff --git a/website/source/docs/providers/fastly/index.html.markdown b/website/source/docs/providers/fastly/index.html.markdown new file mode 100644 index 000000000..fc80da846 --- /dev/null +++ b/website/source/docs/providers/fastly/index.html.markdown @@ -0,0 +1,80 @@ +--- +layout: "fastly" +page_title: "Provider: Fastly" +sidebar_current: "docs-fastly-index" +description: |- + Fastly +--- + +# Fastly Provider + +The Fastly provider is used to interact with the content delivery network (CDN) +provided by Fastly. + +In order to use this Provider, you must have an active account with Fastly. +Pricing and signup information can be found at https://www.fastly.com/signup + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +# Configure the Fastly Provider +provider "fastly" { + api_key = "test" +} + +# Create a Service +resource "fastly_service_v1" "myservice" { + name = "myawesometestservice" + ... +} +``` + +## Authentication + +The Fastly provider offers an API key based method of providing credentials for +authentication. The following methods are supported, in this order, and +explained below: + +- Static API key +- Environment variables + + +### Static API Key ### + +Static credentials can be provided by adding a `api_key` in-line in the +fastly provider block: + +Usage: + +``` +provider "fastly" { + api_key = "test" +} +``` + +The API key for an account can be found on the Account page: https://app.fastly.com/#account + +###Environment variables + +You can provide your API key via `FASTLY_API_KEY` environment variable, +representing your Fastly API key. + +``` +provider "fastly" {} +``` + +Usage: + +``` +$ export FASTLY_API_KEY="afastlyapikey" +$ terraform plan +``` + +## Argument Reference + +The following arguments are supported in the `provider` block: + +* `api_key` - (Optional) This is the API key. It must be provided, but + it can also be sourced from the `FASTLY_API_KEY` environment variable diff --git a/website/source/docs/providers/fastly/r/service_v1.html.markdown b/website/source/docs/providers/fastly/r/service_v1.html.markdown new file mode 100644 index 000000000..8ded8d7fc --- /dev/null +++ b/website/source/docs/providers/fastly/r/service_v1.html.markdown @@ -0,0 +1,136 @@ +--- +layout: "fastly" +page_title: "Fastly: aws_vpc" +sidebar_current: "docs-fastly-resource-service-v1" +description: |- + Provides an Fastly Service +--- + +# fastly\_service\_v1 + +Provides an Fastly Service, representing the configuration for a website, app, +api, or anything else to be served through Fastly. A Service encompasses Domains +and Backends. + +The Service resource requires a domain name that is correctly setup to direct +traffic to the Fastly service. See Fastly's guide on [Adding CNAME Records][2] +on their documentation site for guidance. + +## Example Usage + +Basic usage: + +``` +resource "fastly_service_v1" "demo" { + name = "demofastly" + + domain { + name = "demo.notexample.com" + comment = "demo" + } + + backend { + address = "127.0.0.1" + name = "localhost" + port = 80 + } + + force_destroy = true +} + +``` + +Basic usage with an Amazon S3 Website: + +``` +resource "fastly_service_v1" "demo" { + name = "demofastly" + + domain { + name = "demo.notexample.com" + comment = "demo" + } + + backend { + address = "demo.notexample.com.s3-website-us-west-2.amazonaws.com" + name = "AWS S3 hosting" + port = 80 + } + + default_host = "${aws_s3_bucket.website.name}.s3-website-us-west-2.amazonaws.com" + + force_destroy = true +} + +resource "aws_s3_bucket" "website" { + bucket = "demo.notexample.com" + acl = "public-read" + + website { + index_document = "index.html" + error_document = "error.html" + } +} +``` + +**Note:** For an AWS S3 Bucket, the Backend address is +`.s3-website-.amazonaws.com`. The `default_host` attribute +should be set to `.s3-website-.amazonaws.com`. See the +Fastly documentation on [Amazon S3][1] + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The unique name for the Service to create +* `domain` - (Required) A set of Domain names to serve as entry points for your +Service. Defined below. +* `backend` - (Required) A set of Backends to service requests from your Domains. +Defined below. +* `default_host` - (Optional) The default hostname +* `default_ttl` - (Optional) The default Time-to-live (TTL) for requests +* `force_destroy` - (Optional) Services that are active cannot be destroyed. In +order to destroy the Service, set `force_destroy` to `true`. Default `false`. + + +The `domain` block supports: + +* `name` - (Required) The domain that this Service will respond to +* `comment` - (Optional) An optional comment about the Domain + +The `backend` block supports: + +* `name` - (Required, string) Name for this Backend. Must be unique to this Service +* `address` - (Required, string) An IPv4, hostname, or IPv6 address for the Backend +* `auto_loadbalance` - (Optional, boolean) Denote if this Backend should be +included in the pool of backends that requests are load balanced against. +Default `true` +* `between_bytes_timeout` - (Optional) How long to wait between bytes in milliseconds. Default `10000` +* `connect_timeout` - (Optional) How long to wait for a timeout in milliseconds. +Default `1000` +* `error_threshold` - (Optional) Number of errors to allow before the Backend is marked as down. Default `0` +* `first_byte_timeout` - (Optional) How long to wait for the first bytes in milliseconds. Default `15000` +* `max_conn` - (Optional) Maximum number of connections for this Backend. +Default `200` +* `port` - (Optional) The port number Backend responds on. Default `80` +* `ssl_check_cert` - (Optional) Be strict on checking SSL certs. Default `true` +* `weight` - (Optional) How long to wait for the first bytes in milliseconds. +Default `100` + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the Service +* `name` – Name of this service +* `active_version` - The currently active version of your Fastly Service +* `domain` – Set of Domains. See above for details +* `backend` – Set of Backends. See above for details +* `default_host` – Default host specified +* `default_ttl` - Default TTL +* `force_destroy` - Force the destruction of the Service on delete + + +[1]: https://docs.fastly.com/guides/integrations/amazon-s3 +[2]: https://docs.fastly.com/guides/basic-setup/adding-cname-records + diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 33e5a6d33..f1ec04584 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -193,6 +193,10 @@ Github + > + Fastly + + > Google Cloud diff --git a/website/source/layouts/fastly.erb b/website/source/layouts/fastly.erb new file mode 100644 index 000000000..1958464a0 --- /dev/null +++ b/website/source/layouts/fastly.erb @@ -0,0 +1,28 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> +<% end %>