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", "header", "gzip", } { 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 log.Printf("[DEBUG] Sleeping 7 seconds to allow Fastly Version to be available") 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 } } } if d.HasChange("header") { // Note: we don't utilize the PUT endpoint to update a Header, 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 oh, nh := d.GetChange("header") if oh == nil { oh = new(schema.Set) } if nh == nil { nh = new(schema.Set) } ohs := oh.(*schema.Set) nhs := nh.(*schema.Set) remove := ohs.Difference(nhs).List() add := nhs.Difference(ohs).List() // Delete removed headers for _, dRaw := range remove { df := dRaw.(map[string]interface{}) opts := gofastly.DeleteHeaderInput{ Service: d.Id(), Version: latestVersion, Name: df["name"].(string), } log.Printf("[DEBUG] Fastly Header Removal opts: %#v", opts) err := conn.DeleteHeader(&opts) if err != nil { return err } } // POST new Headers for _, dRaw := range add { opts, err := buildHeader(dRaw.(map[string]interface{})) if err != nil { log.Printf("[DEBUG] Error building Header: %s", err) return err } opts.Service = d.Id() opts.Version = latestVersion log.Printf("[DEBUG] Fastly Header Addition opts: %#v", opts) _, err = conn.CreateHeader(opts) if err != nil { return err } } } // Find differences in Gzips if d.HasChange("gzip") { // Note: we don't utilize the PUT endpoint to update a Gzip rule, 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 og, ng := d.GetChange("gzip") if og == nil { og = new(schema.Set) } if ng == nil { ng = new(schema.Set) } ogs := og.(*schema.Set) ngs := ng.(*schema.Set) remove := ogs.Difference(ngs).List() add := ngs.Difference(ogs).List() // Delete removed gzip rules for _, dRaw := range remove { df := dRaw.(map[string]interface{}) opts := gofastly.DeleteGzipInput{ Service: d.Id(), Version: latestVersion, Name: df["name"].(string), } log.Printf("[DEBUG] Fastly Gzip Removal opts: %#v", opts) err := conn.DeleteGzip(&opts) if err != nil { return err } } // POST new Gzips for _, dRaw := range add { df := dRaw.(map[string]interface{}) opts := gofastly.CreateGzipInput{ Service: d.Id(), Version: latestVersion, Name: df["name"].(string), } // Fastly API will fill in ContentTypes or Extensions with default // values if they are omitted, which is not what we want. Ex: creating a // gzip rule for content types of "text/html", and not supplying any // extensions, will apply automatic values to extensions for css, js, // html. Given Go's nature of default values, and go-fastly's usage of // omitempty for empty strings, we need to pre-fill the ContentTypes and // Extensions with and empty space " " in order to not receive the // default values for each field. This space is checked and then ignored // in the flattenGzips function. // // I've opened a support case with Fastly to find if this is a bug or // feature. If feature, we'll update the go-fastly library to not use // omitempty in the definition. If bug, we'll have to weather it until // they fix it opts.Extensions = " " opts.ContentTypes = " " if v, ok := df["content_types"]; ok { if len(v.(*schema.Set).List()) > 0 { var cl []string for _, c := range v.(*schema.Set).List() { cl = append(cl, c.(string)) } opts.ContentTypes = strings.Join(cl, " ") } } if v, ok := df["extensions"]; ok { if len(v.(*schema.Set).List()) > 0 { var el []string for _, e := range v.(*schema.Set).List() { el = append(el, e.(string)) } opts.Extensions = strings.Join(el, " ") } } log.Printf("[DEBUG] Fastly Gzip Addition opts: %#v", opts) _, err := conn.CreateGzip(&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("[ERR] 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 resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { if err := validateVCLs(d); err != nil { return err } 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", "header", "gzip", "s3logging", "condition", "request_setting", "vcl", } { 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 log.Printf("[DEBUG] Sleeping 7 seconds to allow Fastly Version to be available") 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 } } // Conditions need to be updated first, as they can be referenced by other // configuraiton objects (Backends, Request Headers, etc) // Find difference in Conditions if d.HasChange("condition") { // Note: we don't utilize the PUT endpoint to update these objects, we simply // destroy any that have changed, and create new ones with the updated // values. 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 Fastly Service configuration, this is considered safe oc, nc := d.GetChange("condition") if oc == nil { oc = new(schema.Set) } if nc == nil { nc = new(schema.Set) } ocs := oc.(*schema.Set) ncs := nc.(*schema.Set) removeConditions := ocs.Difference(ncs).List() addConditions := ncs.Difference(ocs).List() // DELETE old Conditions for _, cRaw := range removeConditions { cf := cRaw.(map[string]interface{}) opts := gofastly.DeleteConditionInput{ Service: d.Id(), Version: latestVersion, Name: cf["name"].(string), } log.Printf("[DEBUG] Fastly Conditions Removal opts: %#v", opts) err := conn.DeleteCondition(&opts) if err != nil { return err } } // POST new Conditions for _, cRaw := range addConditions { cf := cRaw.(map[string]interface{}) opts := gofastly.CreateConditionInput{ Service: d.Id(), Version: latestVersion, Name: cf["name"].(string), Type: cf["type"].(string), // need to trim leading/tailing spaces, incase the config has HEREDOC // formatting and contains a trailing new line Statement: strings.TrimSpace(cf["statement"].(string)), Priority: cf["priority"].(int), } log.Printf("[DEBUG] Create Conditions Opts: %#v", opts) _, err := conn.CreateCondition(&opts) if err != nil { return err } } } // Find differences in domains if d.HasChange("domain") { 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") { 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 } } // Find and post new Backends 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 } } } if d.HasChange("header") { oh, nh := d.GetChange("header") if oh == nil { oh = new(schema.Set) } if nh == nil { nh = new(schema.Set) } ohs := oh.(*schema.Set) nhs := nh.(*schema.Set) remove := ohs.Difference(nhs).List() add := nhs.Difference(ohs).List() // Delete removed headers for _, dRaw := range remove { df := dRaw.(map[string]interface{}) opts := gofastly.DeleteHeaderInput{ Service: d.Id(), Version: latestVersion, Name: df["name"].(string), } log.Printf("[DEBUG] Fastly Header removal opts: %#v", opts) err := conn.DeleteHeader(&opts) if err != nil { return err } } // POST new Headers for _, dRaw := range add { opts, err := buildHeader(dRaw.(map[string]interface{})) if err != nil { log.Printf("[DEBUG] Error building Header: %s", err) return err } opts.Service = d.Id() opts.Version = latestVersion log.Printf("[DEBUG] Fastly Header Addition opts: %#v", opts) _, err = conn.CreateHeader(opts) if err != nil { return err } } } // Find differences in Gzips if d.HasChange("gzip") { og, ng := d.GetChange("gzip") if og == nil { og = new(schema.Set) } if ng == nil { ng = new(schema.Set) } ogs := og.(*schema.Set) ngs := ng.(*schema.Set) remove := ogs.Difference(ngs).List() add := ngs.Difference(ogs).List() // Delete removed gzip rules for _, dRaw := range remove { df := dRaw.(map[string]interface{}) opts := gofastly.DeleteGzipInput{ Service: d.Id(), Version: latestVersion, Name: df["name"].(string), } log.Printf("[DEBUG] Fastly Gzip removal opts: %#v", opts) err := conn.DeleteGzip(&opts) if err != nil { return err } } // POST new Gzips for _, dRaw := range add { df := dRaw.(map[string]interface{}) opts := gofastly.CreateGzipInput{ Service: d.Id(), Version: latestVersion, Name: df["name"].(string), } if v, ok := df["content_types"]; ok { if len(v.(*schema.Set).List()) > 0 { var cl []string for _, c := range v.(*schema.Set).List() { cl = append(cl, c.(string)) } opts.ContentTypes = strings.Join(cl, " ") } } if v, ok := df["extensions"]; ok { if len(v.(*schema.Set).List()) > 0 { var el []string for _, e := range v.(*schema.Set).List() { el = append(el, e.(string)) } opts.Extensions = strings.Join(el, " ") } } log.Printf("[DEBUG] Fastly Gzip Addition opts: %#v", opts) _, err := conn.CreateGzip(&opts) if err != nil { return err } } } // find difference in s3logging if d.HasChange("s3logging") { os, ns := d.GetChange("s3logging") if os == nil { os = new(schema.Set) } if ns == nil { ns = new(schema.Set) } oss := os.(*schema.Set) nss := ns.(*schema.Set) removeS3Logging := oss.Difference(nss).List() addS3Logging := nss.Difference(oss).List() // DELETE old S3 Log configurations for _, sRaw := range removeS3Logging { sf := sRaw.(map[string]interface{}) opts := gofastly.DeleteS3Input{ Service: d.Id(), Version: latestVersion, Name: sf["name"].(string), } log.Printf("[DEBUG] Fastly S3 Logging removal opts: %#v", opts) err := conn.DeleteS3(&opts) if err != nil { return err } } // POST new/updated S3 Logging for _, sRaw := range addS3Logging { sf := sRaw.(map[string]interface{}) // Fastly API will not error if these are omitted, so we throw an error // if any of these are empty for _, sk := range []string{"s3_access_key", "s3_secret_key"} { if sf[sk].(string) == "" { return fmt.Errorf("[ERR] No %s found for S3 Log stream setup for Service (%s)", sk, d.Id()) } } opts := gofastly.CreateS3Input{ Service: d.Id(), Version: latestVersion, Name: sf["name"].(string), BucketName: sf["bucket_name"].(string), AccessKey: sf["s3_access_key"].(string), SecretKey: sf["s3_secret_key"].(string), Period: uint(sf["period"].(int)), GzipLevel: uint(sf["gzip_level"].(int)), Domain: sf["domain"].(string), Path: sf["path"].(string), Format: sf["format"].(string), TimestampFormat: sf["timestamp_format"].(string), } log.Printf("[DEBUG] Create S3 Logging Opts: %#v", opts) _, err := conn.CreateS3(&opts) if err != nil { return err } } } // find difference in request settings if d.HasChange("request_setting") { os, ns := d.GetChange("request_setting") if os == nil { os = new(schema.Set) } if ns == nil { ns = new(schema.Set) } ors := os.(*schema.Set) nrs := ns.(*schema.Set) removeRequestSettings := ors.Difference(nrs).List() addRequestSettings := nrs.Difference(ors).List() // DELETE old Request Settings configurations for _, sRaw := range removeRequestSettings { sf := sRaw.(map[string]interface{}) opts := gofastly.DeleteRequestSettingInput{ Service: d.Id(), Version: latestVersion, Name: sf["name"].(string), } log.Printf("[DEBUG] Fastly Request Setting removal opts: %#v", opts) err := conn.DeleteRequestSetting(&opts) if err != nil { return err } } // POST new/updated Request Setting for _, sRaw := range addRequestSettings { opts, err := buildRequestSetting(sRaw.(map[string]interface{})) if err != nil { log.Printf("[DEBUG] Error building Requset Setting: %s", err) return err } opts.Service = d.Id() opts.Version = latestVersion log.Printf("[DEBUG] Create Request Setting Opts: %#v", opts) _, err = conn.CreateRequestSetting(opts) if err != nil { return err } } } // Find differences in VCLs if d.HasChange("vcl") { // Note: as above with Gzip and S3 logging, we don't utilize the PUT // endpoint to update a VCL, we simply destroy it and create a new one. oldVCLVal, newVCLVal := d.GetChange("vcl") if oldVCLVal == nil { oldVCLVal = new(schema.Set) } if newVCLVal == nil { newVCLVal = new(schema.Set) } oldVCLSet := oldVCLVal.(*schema.Set) newVCLSet := newVCLVal.(*schema.Set) remove := oldVCLSet.Difference(newVCLSet).List() add := newVCLSet.Difference(oldVCLSet).List() // Delete removed VCL configurations for _, dRaw := range remove { df := dRaw.(map[string]interface{}) opts := gofastly.DeleteVCLInput{ Service: d.Id(), Version: latestVersion, Name: df["name"].(string), } log.Printf("[DEBUG] Fastly VCL Removal opts: %#v", opts) err := conn.DeleteVCL(&opts) if err != nil { return err } } // POST new VCL configurations for _, dRaw := range add { df := dRaw.(map[string]interface{}) opts := gofastly.CreateVCLInput{ Service: d.Id(), Version: latestVersion, Name: df["name"].(string), Content: df["content"].(string), } log.Printf("[DEBUG] Fastly VCL Addition opts: %#v", opts) _, err := conn.CreateVCL(&opts) if err != nil { return err } // if this new VCL is the main if df["main"].(bool) { opts := gofastly.ActivateVCLInput{ Service: d.Id(), Version: latestVersion, Name: df["name"].(string), } log.Printf("[DEBUG] Fastly VCL activation opts: %#v", opts) _, err := conn.ActivateVCL(&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("[ERR] 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) }