github.com/anfernee/terraform@v0.6.16-0.20160430000239-06e5085a92f2/builtin/providers/fastly/resource_fastly_service_v1.go (about)

     1  package fastly
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/hashicorp/terraform/helper/schema"
    11  	gofastly "github.com/sethvargo/go-fastly"
    12  )
    13  
    14  var fastlyNoServiceFoundErr = errors.New("No matching Fastly Service found")
    15  
    16  func resourceServiceV1() *schema.Resource {
    17  	return &schema.Resource{
    18  		Create: resourceServiceV1Create,
    19  		Read:   resourceServiceV1Read,
    20  		Update: resourceServiceV1Update,
    21  		Delete: resourceServiceV1Delete,
    22  
    23  		Schema: map[string]*schema.Schema{
    24  			"name": &schema.Schema{
    25  				Type:        schema.TypeString,
    26  				Required:    true,
    27  				Description: "Unique name for this Service",
    28  			},
    29  
    30  			// Active Version represents the currently activated version in Fastly. In
    31  			// Terraform, we abstract this number away from the users and manage
    32  			// creating and activating. It's used internally, but also exported for
    33  			// users to see.
    34  			"active_version": &schema.Schema{
    35  				Type:     schema.TypeString,
    36  				Computed: true,
    37  			},
    38  
    39  			"domain": &schema.Schema{
    40  				Type:     schema.TypeSet,
    41  				Required: true,
    42  				Elem: &schema.Resource{
    43  					Schema: map[string]*schema.Schema{
    44  						"name": &schema.Schema{
    45  							Type:        schema.TypeString,
    46  							Required:    true,
    47  							Description: "The domain that this Service will respond to",
    48  						},
    49  
    50  						"comment": &schema.Schema{
    51  							Type:     schema.TypeString,
    52  							Optional: true,
    53  						},
    54  					},
    55  				},
    56  			},
    57  
    58  			"default_ttl": &schema.Schema{
    59  				Type:        schema.TypeInt,
    60  				Optional:    true,
    61  				Default:     3600,
    62  				Description: "The default Time-to-live (TTL) for the version",
    63  			},
    64  
    65  			"default_host": &schema.Schema{
    66  				Type:        schema.TypeString,
    67  				Optional:    true,
    68  				Computed:    true,
    69  				Description: "The default hostname for the version",
    70  			},
    71  
    72  			"backend": &schema.Schema{
    73  				Type:     schema.TypeSet,
    74  				Required: true,
    75  				Elem: &schema.Resource{
    76  					Schema: map[string]*schema.Schema{
    77  						// required fields
    78  						"name": &schema.Schema{
    79  							Type:        schema.TypeString,
    80  							Required:    true,
    81  							Description: "A name for this Backend",
    82  						},
    83  						"address": &schema.Schema{
    84  							Type:        schema.TypeString,
    85  							Required:    true,
    86  							Description: "An IPv4, hostname, or IPv6 address for the Backend",
    87  						},
    88  						// Optional fields, defaults where they exist
    89  						"auto_loadbalance": &schema.Schema{
    90  							Type:        schema.TypeBool,
    91  							Optional:    true,
    92  							Default:     true,
    93  							Description: "Should this Backend be load balanced",
    94  						},
    95  						"between_bytes_timeout": &schema.Schema{
    96  							Type:        schema.TypeInt,
    97  							Optional:    true,
    98  							Default:     10000,
    99  							Description: "How long to wait between bytes in milliseconds",
   100  						},
   101  						"connect_timeout": &schema.Schema{
   102  							Type:        schema.TypeInt,
   103  							Optional:    true,
   104  							Default:     1000,
   105  							Description: "How long to wait for a timeout in milliseconds",
   106  						},
   107  						"error_threshold": &schema.Schema{
   108  							Type:        schema.TypeInt,
   109  							Optional:    true,
   110  							Default:     0,
   111  							Description: "Number of errors to allow before the Backend is marked as down",
   112  						},
   113  						"first_byte_timeout": &schema.Schema{
   114  							Type:        schema.TypeInt,
   115  							Optional:    true,
   116  							Default:     15000,
   117  							Description: "How long to wait for the first bytes in milliseconds",
   118  						},
   119  						"max_conn": &schema.Schema{
   120  							Type:        schema.TypeInt,
   121  							Optional:    true,
   122  							Default:     200,
   123  							Description: "Maximum number of connections for this Backend",
   124  						},
   125  						"port": &schema.Schema{
   126  							Type:        schema.TypeInt,
   127  							Optional:    true,
   128  							Default:     80,
   129  							Description: "The port number Backend responds on. Default 80",
   130  						},
   131  						"ssl_check_cert": &schema.Schema{
   132  							Type:        schema.TypeBool,
   133  							Optional:    true,
   134  							Default:     true,
   135  							Description: "Be strict on checking SSL certs",
   136  						},
   137  						// UseSSL is something we want to support in the future, but
   138  						// requires SSL setup we don't yet have
   139  						// TODO: Provide all SSL fields from https://docs.fastly.com/api/config#backend
   140  						// "use_ssl": &schema.Schema{
   141  						// 	Type:        schema.TypeBool,
   142  						// 	Optional:    true,
   143  						// 	Default:     false,
   144  						// 	Description: "Whether or not to use SSL to reach the Backend",
   145  						// },
   146  						"weight": &schema.Schema{
   147  							Type:        schema.TypeInt,
   148  							Optional:    true,
   149  							Default:     100,
   150  							Description: "The portion of traffic to send to a specific origins. Each origin receives weight/total of the traffic.",
   151  						},
   152  					},
   153  				},
   154  			},
   155  
   156  			"force_destroy": &schema.Schema{
   157  				Type:     schema.TypeBool,
   158  				Optional: true,
   159  			},
   160  
   161  			"gzip": &schema.Schema{
   162  				Type:     schema.TypeSet,
   163  				Optional: true,
   164  				Elem: &schema.Resource{
   165  					Schema: map[string]*schema.Schema{
   166  						// required fields
   167  						"name": &schema.Schema{
   168  							Type:        schema.TypeString,
   169  							Required:    true,
   170  							Description: "A name to refer to this gzip condition",
   171  						},
   172  						// optional fields
   173  						"content_types": &schema.Schema{
   174  							Type:        schema.TypeSet,
   175  							Optional:    true,
   176  							Description: "Content types to apply automatic gzip to",
   177  							Elem:        &schema.Schema{Type: schema.TypeString},
   178  						},
   179  						"extensions": &schema.Schema{
   180  							Type:        schema.TypeSet,
   181  							Optional:    true,
   182  							Description: "File extensions to apply automatic gzip to. Do not include '.'",
   183  							Elem:        &schema.Schema{Type: schema.TypeString},
   184  						},
   185  						// These fields represent Fastly options that Terraform does not
   186  						// currently support
   187  						"cache_condition": &schema.Schema{
   188  							Type:        schema.TypeString,
   189  							Computed:    true,
   190  							Description: "Optional name of a CacheCondition to apply.",
   191  						},
   192  					},
   193  				},
   194  			},
   195  
   196  			"header": &schema.Schema{
   197  				Type:     schema.TypeSet,
   198  				Optional: true,
   199  				Elem: &schema.Resource{
   200  					Schema: map[string]*schema.Schema{
   201  						// required fields
   202  						"name": &schema.Schema{
   203  							Type:        schema.TypeString,
   204  							Required:    true,
   205  							Description: "A name to refer to this Header object",
   206  						},
   207  						"action": &schema.Schema{
   208  							Type:        schema.TypeString,
   209  							Required:    true,
   210  							Description: "One of set, append, delete, regex, or regex_repeat",
   211  							ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
   212  								var found bool
   213  								for _, t := range []string{"set", "append", "delete", "regex", "regex_repeat"} {
   214  									if v.(string) == t {
   215  										found = true
   216  									}
   217  								}
   218  								if !found {
   219  									es = append(es, fmt.Errorf(
   220  										"Fastly Header action is case sensitive and must be one of 'set', 'append', 'delete', 'regex', or 'regex_repeat'; found: %s", v.(string)))
   221  								}
   222  								return
   223  							},
   224  						},
   225  						"type": &schema.Schema{
   226  							Type:        schema.TypeString,
   227  							Required:    true,
   228  							Description: "Type to manipulate: request, fetch, cache, response",
   229  							ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
   230  								var found bool
   231  								for _, t := range []string{"request", "fetch", "cache", "response"} {
   232  									if v.(string) == t {
   233  										found = true
   234  									}
   235  								}
   236  								if !found {
   237  									es = append(es, fmt.Errorf(
   238  										"Fastly Header type is case sensitive and must be one of 'request', 'fetch', 'cache', or 'response'; found: %s", v.(string)))
   239  								}
   240  								return
   241  							},
   242  						},
   243  						"destination": &schema.Schema{
   244  							Type:        schema.TypeString,
   245  							Required:    true,
   246  							Description: "Header this affects",
   247  						},
   248  						// Optional fields, defaults where they exist
   249  						"ignore_if_set": &schema.Schema{
   250  							Type:        schema.TypeBool,
   251  							Optional:    true,
   252  							Default:     false,
   253  							Description: "Don't add the header if it is already. (Only applies to 'set' action.). Default `false`",
   254  						},
   255  						"source": &schema.Schema{
   256  							Type:        schema.TypeString,
   257  							Optional:    true,
   258  							Computed:    true,
   259  							Description: "Variable to be used as a source for the header content (Does not apply to 'delete' action.)",
   260  						},
   261  						"regex": &schema.Schema{
   262  							Type:        schema.TypeString,
   263  							Optional:    true,
   264  							Computed:    true,
   265  							Description: "Regular expression to use (Only applies to 'regex' and 'regex_repeat' actions.)",
   266  						},
   267  						"substitution": &schema.Schema{
   268  							Type:        schema.TypeString,
   269  							Optional:    true,
   270  							Computed:    true,
   271  							Description: "Value to substitute in place of regular expression. (Only applies to 'regex' and 'regex_repeat'.)",
   272  						},
   273  						"priority": &schema.Schema{
   274  							Type:        schema.TypeInt,
   275  							Optional:    true,
   276  							Default:     100,
   277  							Description: "Lower priorities execute first. (Default: 100.)",
   278  						},
   279  						// These fields represent Fastly options that Terraform does not
   280  						// currently support
   281  						"request_condition": &schema.Schema{
   282  							Type:        schema.TypeString,
   283  							Computed:    true,
   284  							Description: "Optional name of a RequestCondition to apply.",
   285  						},
   286  						"cache_condition": &schema.Schema{
   287  							Type:        schema.TypeString,
   288  							Computed:    true,
   289  							Description: "Optional name of a CacheCondition to apply.",
   290  						},
   291  						"response_condition": &schema.Schema{
   292  							Type:        schema.TypeString,
   293  							Computed:    true,
   294  							Description: "Optional name of a ResponseCondition to apply.",
   295  						},
   296  					},
   297  				},
   298  			},
   299  
   300  			"s3logging": &schema.Schema{
   301  				Type:     schema.TypeSet,
   302  				Optional: true,
   303  				Elem: &schema.Resource{
   304  					Schema: map[string]*schema.Schema{
   305  						// Required fields
   306  						"name": &schema.Schema{
   307  							Type:        schema.TypeString,
   308  							Required:    true,
   309  							Description: "Unique name to refer to this logging setup",
   310  						},
   311  						"bucket_name": &schema.Schema{
   312  							Type:        schema.TypeString,
   313  							Required:    true,
   314  							Description: "S3 Bucket name to store logs in",
   315  						},
   316  						"s3_access_key": &schema.Schema{
   317  							Type:        schema.TypeString,
   318  							Optional:    true,
   319  							DefaultFunc: schema.EnvDefaultFunc("FASTLY_S3_ACCESS_KEY", ""),
   320  							Description: "AWS Access Key",
   321  						},
   322  						"s3_secret_key": &schema.Schema{
   323  							Type:        schema.TypeString,
   324  							Optional:    true,
   325  							DefaultFunc: schema.EnvDefaultFunc("FASTLY_S3_SECRET_KEY", ""),
   326  							Description: "AWS Secret Key",
   327  						},
   328  						// Optional fields
   329  						"path": &schema.Schema{
   330  							Type:        schema.TypeString,
   331  							Optional:    true,
   332  							Description: "Path to store the files. Must end with a trailing slash",
   333  						},
   334  						"domain": &schema.Schema{
   335  							Type:        schema.TypeString,
   336  							Optional:    true,
   337  							Description: "Bucket endpoint",
   338  						},
   339  						"gzip_level": &schema.Schema{
   340  							Type:        schema.TypeInt,
   341  							Optional:    true,
   342  							Default:     0,
   343  							Description: "Gzip Compression level",
   344  						},
   345  						"period": &schema.Schema{
   346  							Type:        schema.TypeInt,
   347  							Optional:    true,
   348  							Default:     3600,
   349  							Description: "How frequently the logs should be transferred, in seconds (Default 3600)",
   350  						},
   351  						"format": &schema.Schema{
   352  							Type:        schema.TypeString,
   353  							Optional:    true,
   354  							Default:     "%h %l %u %t %r %>s",
   355  							Description: "Apache-style string or VCL variables to use for log formatting",
   356  						},
   357  						"timestamp_format": &schema.Schema{
   358  							Type:        schema.TypeString,
   359  							Optional:    true,
   360  							Default:     "%Y-%m-%dT%H:%M:%S.000",
   361  							Description: "specified timestamp formatting (default `%Y-%m-%dT%H:%M:%S.000`)",
   362  						},
   363  					},
   364  				},
   365  			},
   366  		},
   367  	}
   368  }
   369  
   370  func resourceServiceV1Create(d *schema.ResourceData, meta interface{}) error {
   371  	conn := meta.(*FastlyClient).conn
   372  	service, err := conn.CreateService(&gofastly.CreateServiceInput{
   373  		Name:    d.Get("name").(string),
   374  		Comment: "Managed by Terraform",
   375  	})
   376  
   377  	if err != nil {
   378  		return err
   379  	}
   380  
   381  	d.SetId(service.ID)
   382  	return resourceServiceV1Update(d, meta)
   383  }
   384  
   385  func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
   386  	conn := meta.(*FastlyClient).conn
   387  
   388  	// Update Name. No new verions is required for this
   389  	if d.HasChange("name") {
   390  		_, err := conn.UpdateService(&gofastly.UpdateServiceInput{
   391  			ID:   d.Id(),
   392  			Name: d.Get("name").(string),
   393  		})
   394  		if err != nil {
   395  			return err
   396  		}
   397  	}
   398  
   399  	// Once activated, Versions are locked and become immutable. This is true for
   400  	// versions that are no longer active. For Domains, Backends, DefaultHost and
   401  	// DefaultTTL, a new Version must be created first, and updates posted to that
   402  	// Version. Loop these attributes and determine if we need to create a new version first
   403  	var needsChange bool
   404  	for _, v := range []string{
   405  		"domain",
   406  		"backend",
   407  		"default_host",
   408  		"default_ttl",
   409  		"header",
   410  		"gzip",
   411  		"s3logging",
   412  	} {
   413  		if d.HasChange(v) {
   414  			needsChange = true
   415  		}
   416  	}
   417  
   418  	if needsChange {
   419  		latestVersion := d.Get("active_version").(string)
   420  		if latestVersion == "" {
   421  			// If the service was just created, there is an empty Version 1 available
   422  			// that is unlocked and can be updated
   423  			latestVersion = "1"
   424  		} else {
   425  			// Clone the latest version, giving us an unlocked version we can modify
   426  			log.Printf("[DEBUG] Creating clone of version (%s) for updates", latestVersion)
   427  			newVersion, err := conn.CloneVersion(&gofastly.CloneVersionInput{
   428  				Service: d.Id(),
   429  				Version: latestVersion,
   430  			})
   431  			if err != nil {
   432  				return err
   433  			}
   434  
   435  			// The new version number is named "Number", but it's actually a string
   436  			latestVersion = newVersion.Number
   437  
   438  			// New versions are not immediately found in the API, or are not
   439  			// immediately mutable, so we need to sleep a few and let Fastly ready
   440  			// itself. Typically, 7 seconds is enough
   441  			log.Printf("[DEBUG] Sleeping 7 seconds to allow Fastly Version to be available")
   442  			time.Sleep(7 * time.Second)
   443  		}
   444  
   445  		// update general settings
   446  		if d.HasChange("default_host") || d.HasChange("default_ttl") {
   447  			opts := gofastly.UpdateSettingsInput{
   448  				Service: d.Id(),
   449  				Version: latestVersion,
   450  				// default_ttl has the same default value of 3600 that is provided by
   451  				// the Fastly API, so it's safe to include here
   452  				DefaultTTL: uint(d.Get("default_ttl").(int)),
   453  			}
   454  
   455  			if attr, ok := d.GetOk("default_host"); ok {
   456  				opts.DefaultHost = attr.(string)
   457  			}
   458  
   459  			log.Printf("[DEBUG] Update Settings opts: %#v", opts)
   460  			_, err := conn.UpdateSettings(&opts)
   461  			if err != nil {
   462  				return err
   463  			}
   464  		}
   465  
   466  		// Find differences in domains
   467  		if d.HasChange("domain") {
   468  			// Note: we don't utilize the PUT endpoint to update a Domain, we simply
   469  			// destroy it and create a new one. This is how Terraform works with nested
   470  			// sub resources, we only get the full diff not a partial set item diff.
   471  			// Because this is done on a new version of the configuration, this is
   472  			// considered safe
   473  			od, nd := d.GetChange("domain")
   474  			if od == nil {
   475  				od = new(schema.Set)
   476  			}
   477  			if nd == nil {
   478  				nd = new(schema.Set)
   479  			}
   480  
   481  			ods := od.(*schema.Set)
   482  			nds := nd.(*schema.Set)
   483  
   484  			remove := ods.Difference(nds).List()
   485  			add := nds.Difference(ods).List()
   486  
   487  			// Delete removed domains
   488  			for _, dRaw := range remove {
   489  				df := dRaw.(map[string]interface{})
   490  				opts := gofastly.DeleteDomainInput{
   491  					Service: d.Id(),
   492  					Version: latestVersion,
   493  					Name:    df["name"].(string),
   494  				}
   495  
   496  				log.Printf("[DEBUG] Fastly Domain Removal opts: %#v", opts)
   497  				err := conn.DeleteDomain(&opts)
   498  				if err != nil {
   499  					return err
   500  				}
   501  			}
   502  
   503  			// POST new Domains
   504  			for _, dRaw := range add {
   505  				df := dRaw.(map[string]interface{})
   506  				opts := gofastly.CreateDomainInput{
   507  					Service: d.Id(),
   508  					Version: latestVersion,
   509  					Name:    df["name"].(string),
   510  				}
   511  
   512  				if v, ok := df["comment"]; ok {
   513  					opts.Comment = v.(string)
   514  				}
   515  
   516  				log.Printf("[DEBUG] Fastly Domain Addition opts: %#v", opts)
   517  				_, err := conn.CreateDomain(&opts)
   518  				if err != nil {
   519  					return err
   520  				}
   521  			}
   522  		}
   523  
   524  		// find difference in backends
   525  		if d.HasChange("backend") {
   526  			// POST new Backends
   527  			// Note: we don't utilize the PUT endpoint to update a Backend, we simply
   528  			// destroy it and create a new one. This is how Terraform works with nested
   529  			// sub resources, we only get the full diff not a partial set item diff.
   530  			// Because this is done on a new version of the configuration, this is
   531  			// considered safe
   532  			ob, nb := d.GetChange("backend")
   533  			if ob == nil {
   534  				ob = new(schema.Set)
   535  			}
   536  			if nb == nil {
   537  				nb = new(schema.Set)
   538  			}
   539  
   540  			obs := ob.(*schema.Set)
   541  			nbs := nb.(*schema.Set)
   542  			removeBackends := obs.Difference(nbs).List()
   543  			addBackends := nbs.Difference(obs).List()
   544  
   545  			// DELETE old Backends
   546  			for _, bRaw := range removeBackends {
   547  				bf := bRaw.(map[string]interface{})
   548  				opts := gofastly.DeleteBackendInput{
   549  					Service: d.Id(),
   550  					Version: latestVersion,
   551  					Name:    bf["name"].(string),
   552  				}
   553  
   554  				log.Printf("[DEBUG] Fastly Backend Removal opts: %#v", opts)
   555  				err := conn.DeleteBackend(&opts)
   556  				if err != nil {
   557  					return err
   558  				}
   559  			}
   560  
   561  			for _, dRaw := range addBackends {
   562  				df := dRaw.(map[string]interface{})
   563  				opts := gofastly.CreateBackendInput{
   564  					Service:             d.Id(),
   565  					Version:             latestVersion,
   566  					Name:                df["name"].(string),
   567  					Address:             df["address"].(string),
   568  					AutoLoadbalance:     df["auto_loadbalance"].(bool),
   569  					SSLCheckCert:        df["ssl_check_cert"].(bool),
   570  					Port:                uint(df["port"].(int)),
   571  					BetweenBytesTimeout: uint(df["between_bytes_timeout"].(int)),
   572  					ConnectTimeout:      uint(df["connect_timeout"].(int)),
   573  					ErrorThreshold:      uint(df["error_threshold"].(int)),
   574  					FirstByteTimeout:    uint(df["first_byte_timeout"].(int)),
   575  					MaxConn:             uint(df["max_conn"].(int)),
   576  					Weight:              uint(df["weight"].(int)),
   577  				}
   578  
   579  				log.Printf("[DEBUG] Create Backend Opts: %#v", opts)
   580  				_, err := conn.CreateBackend(&opts)
   581  				if err != nil {
   582  					return err
   583  				}
   584  			}
   585  		}
   586  
   587  		if d.HasChange("header") {
   588  			// Note: we don't utilize the PUT endpoint to update a Header, we simply
   589  			// destroy it and create a new one. This is how Terraform works with nested
   590  			// sub resources, we only get the full diff not a partial set item diff.
   591  			// Because this is done on a new version of the configuration, this is
   592  			// considered safe
   593  			oh, nh := d.GetChange("header")
   594  			if oh == nil {
   595  				oh = new(schema.Set)
   596  			}
   597  			if nh == nil {
   598  				nh = new(schema.Set)
   599  			}
   600  
   601  			ohs := oh.(*schema.Set)
   602  			nhs := nh.(*schema.Set)
   603  
   604  			remove := ohs.Difference(nhs).List()
   605  			add := nhs.Difference(ohs).List()
   606  
   607  			// Delete removed headers
   608  			for _, dRaw := range remove {
   609  				df := dRaw.(map[string]interface{})
   610  				opts := gofastly.DeleteHeaderInput{
   611  					Service: d.Id(),
   612  					Version: latestVersion,
   613  					Name:    df["name"].(string),
   614  				}
   615  
   616  				log.Printf("[DEBUG] Fastly Header Removal opts: %#v", opts)
   617  				err := conn.DeleteHeader(&opts)
   618  				if err != nil {
   619  					return err
   620  				}
   621  			}
   622  
   623  			// POST new Headers
   624  			for _, dRaw := range add {
   625  				opts, err := buildHeader(dRaw.(map[string]interface{}))
   626  				if err != nil {
   627  					log.Printf("[DEBUG] Error building Header: %s", err)
   628  					return err
   629  				}
   630  				opts.Service = d.Id()
   631  				opts.Version = latestVersion
   632  
   633  				log.Printf("[DEBUG] Fastly Header Addition opts: %#v", opts)
   634  				_, err = conn.CreateHeader(opts)
   635  				if err != nil {
   636  					return err
   637  				}
   638  			}
   639  		}
   640  
   641  		// Find differences in Gzips
   642  		if d.HasChange("gzip") {
   643  			// Note: we don't utilize the PUT endpoint to update a Gzip rule, we simply
   644  			// destroy it and create a new one. This is how Terraform works with nested
   645  			// sub resources, we only get the full diff not a partial set item diff.
   646  			// Because this is done on a new version of the configuration, this is
   647  			// considered safe
   648  			og, ng := d.GetChange("gzip")
   649  			if og == nil {
   650  				og = new(schema.Set)
   651  			}
   652  			if ng == nil {
   653  				ng = new(schema.Set)
   654  			}
   655  
   656  			ogs := og.(*schema.Set)
   657  			ngs := ng.(*schema.Set)
   658  
   659  			remove := ogs.Difference(ngs).List()
   660  			add := ngs.Difference(ogs).List()
   661  
   662  			// Delete removed gzip rules
   663  			for _, dRaw := range remove {
   664  				df := dRaw.(map[string]interface{})
   665  				opts := gofastly.DeleteGzipInput{
   666  					Service: d.Id(),
   667  					Version: latestVersion,
   668  					Name:    df["name"].(string),
   669  				}
   670  
   671  				log.Printf("[DEBUG] Fastly Gzip Removal opts: %#v", opts)
   672  				err := conn.DeleteGzip(&opts)
   673  				if err != nil {
   674  					return err
   675  				}
   676  			}
   677  
   678  			// POST new Gzips
   679  			for _, dRaw := range add {
   680  				df := dRaw.(map[string]interface{})
   681  				opts := gofastly.CreateGzipInput{
   682  					Service: d.Id(),
   683  					Version: latestVersion,
   684  					Name:    df["name"].(string),
   685  				}
   686  
   687  				if v, ok := df["content_types"]; ok {
   688  					if len(v.(*schema.Set).List()) > 0 {
   689  						var cl []string
   690  						for _, c := range v.(*schema.Set).List() {
   691  							cl = append(cl, c.(string))
   692  						}
   693  						opts.ContentTypes = strings.Join(cl, " ")
   694  					}
   695  				}
   696  
   697  				if v, ok := df["extensions"]; ok {
   698  					if len(v.(*schema.Set).List()) > 0 {
   699  						var el []string
   700  						for _, e := range v.(*schema.Set).List() {
   701  							el = append(el, e.(string))
   702  						}
   703  						opts.Extensions = strings.Join(el, " ")
   704  					}
   705  				}
   706  
   707  				log.Printf("[DEBUG] Fastly Gzip Addition opts: %#v", opts)
   708  				_, err := conn.CreateGzip(&opts)
   709  				if err != nil {
   710  					return err
   711  				}
   712  			}
   713  		}
   714  
   715  		// find difference in s3logging
   716  		if d.HasChange("s3logging") {
   717  			// POST new Logging
   718  			// Note: we don't utilize the PUT endpoint to update a S3 Logs, we simply
   719  			// destroy it and create a new one. This is how Terraform works with nested
   720  			// sub resources, we only get the full diff not a partial set item diff.
   721  			// Because this is done on a new version of the configuration, this is
   722  			// considered safe
   723  			os, ns := d.GetChange("s3logging")
   724  			if os == nil {
   725  				os = new(schema.Set)
   726  			}
   727  			if ns == nil {
   728  				ns = new(schema.Set)
   729  			}
   730  
   731  			oss := os.(*schema.Set)
   732  			nss := ns.(*schema.Set)
   733  			removeS3Logging := oss.Difference(nss).List()
   734  			addS3Logging := nss.Difference(oss).List()
   735  
   736  			// DELETE old S3 Log configurations
   737  			for _, sRaw := range removeS3Logging {
   738  				sf := sRaw.(map[string]interface{})
   739  				opts := gofastly.DeleteS3Input{
   740  					Service: d.Id(),
   741  					Version: latestVersion,
   742  					Name:    sf["name"].(string),
   743  				}
   744  
   745  				log.Printf("[DEBUG] Fastly S3 Logging Removal opts: %#v", opts)
   746  				err := conn.DeleteS3(&opts)
   747  				if err != nil {
   748  					return err
   749  				}
   750  			}
   751  
   752  			// POST new/updated S3 Logging
   753  			for _, sRaw := range addS3Logging {
   754  				sf := sRaw.(map[string]interface{})
   755  
   756  				// Fastly API will not error if these are omitted, so we throw an error
   757  				// if any of these are empty
   758  				for _, sk := range []string{"s3_access_key", "s3_secret_key"} {
   759  					if sf[sk].(string) == "" {
   760  						return fmt.Errorf("[ERR] No %s found for S3 Log stream setup for Service (%s)", sk, d.Id())
   761  					}
   762  				}
   763  
   764  				opts := gofastly.CreateS3Input{
   765  					Service:         d.Id(),
   766  					Version:         latestVersion,
   767  					Name:            sf["name"].(string),
   768  					BucketName:      sf["bucket_name"].(string),
   769  					AccessKey:       sf["s3_access_key"].(string),
   770  					SecretKey:       sf["s3_secret_key"].(string),
   771  					Period:          uint(sf["period"].(int)),
   772  					GzipLevel:       uint(sf["gzip_level"].(int)),
   773  					Domain:          sf["domain"].(string),
   774  					Path:            sf["path"].(string),
   775  					Format:          sf["format"].(string),
   776  					TimestampFormat: sf["timestamp_format"].(string),
   777  				}
   778  
   779  				log.Printf("[DEBUG] Create S3 Logging Opts: %#v", opts)
   780  				_, err := conn.CreateS3(&opts)
   781  				if err != nil {
   782  					return err
   783  				}
   784  			}
   785  		}
   786  
   787  		// validate version
   788  		log.Printf("[DEBUG] Validating Fastly Service (%s), Version (%s)", d.Id(), latestVersion)
   789  		valid, msg, err := conn.ValidateVersion(&gofastly.ValidateVersionInput{
   790  			Service: d.Id(),
   791  			Version: latestVersion,
   792  		})
   793  
   794  		if err != nil {
   795  			return fmt.Errorf("[ERR] Error checking validation: %s", err)
   796  		}
   797  
   798  		if !valid {
   799  			return fmt.Errorf("[ERR] Invalid configuration for Fastly Service (%s): %s", d.Id(), msg)
   800  		}
   801  
   802  		log.Printf("[DEBUG] Activating Fastly Service (%s), Version (%s)", d.Id(), latestVersion)
   803  		_, err = conn.ActivateVersion(&gofastly.ActivateVersionInput{
   804  			Service: d.Id(),
   805  			Version: latestVersion,
   806  		})
   807  		if err != nil {
   808  			return fmt.Errorf("[ERR] Error activating version (%s): %s", latestVersion, err)
   809  		}
   810  
   811  		// Only if the version is valid and activated do we set the active_version.
   812  		// This prevents us from getting stuck in cloning an invalid version
   813  		d.Set("active_version", latestVersion)
   814  	}
   815  
   816  	return resourceServiceV1Read(d, meta)
   817  }
   818  
   819  func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error {
   820  	conn := meta.(*FastlyClient).conn
   821  
   822  	// Find the Service. Discard the service because we need the ServiceDetails,
   823  	// not just a Service record
   824  	_, err := findService(d.Id(), meta)
   825  	if err != nil {
   826  		switch err {
   827  		case fastlyNoServiceFoundErr:
   828  			log.Printf("[WARN] %s for ID (%s)", err, d.Id())
   829  			d.SetId("")
   830  			return nil
   831  		default:
   832  			return err
   833  		}
   834  	}
   835  
   836  	s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{
   837  		ID: d.Id(),
   838  	})
   839  
   840  	if err != nil {
   841  		return err
   842  	}
   843  
   844  	d.Set("name", s.Name)
   845  	d.Set("active_version", s.ActiveVersion.Number)
   846  
   847  	// If CreateService succeeds, but initial updates to the Service fail, we'll
   848  	// have an empty ActiveService version (no version is active, so we can't
   849  	// query for information on it)
   850  	if s.ActiveVersion.Number != "" {
   851  		settingsOpts := gofastly.GetSettingsInput{
   852  			Service: d.Id(),
   853  			Version: s.ActiveVersion.Number,
   854  		}
   855  		if settings, err := conn.GetSettings(&settingsOpts); err == nil {
   856  			d.Set("default_host", settings.DefaultHost)
   857  			d.Set("default_ttl", settings.DefaultTTL)
   858  		} else {
   859  			return fmt.Errorf("[ERR] Error looking up Version settings for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
   860  		}
   861  
   862  		// TODO: update go-fastly to support an ActiveVersion struct, which contains
   863  		// domain and backend info in the response. Here we do 2 additional queries
   864  		// to find out that info
   865  		log.Printf("[DEBUG] Refreshing Domains for (%s)", d.Id())
   866  		domainList, err := conn.ListDomains(&gofastly.ListDomainsInput{
   867  			Service: d.Id(),
   868  			Version: s.ActiveVersion.Number,
   869  		})
   870  
   871  		if err != nil {
   872  			return fmt.Errorf("[ERR] Error looking up Domains for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
   873  		}
   874  
   875  		// Refresh Domains
   876  		dl := flattenDomains(domainList)
   877  
   878  		if err := d.Set("domain", dl); err != nil {
   879  			log.Printf("[WARN] Error setting Domains for (%s): %s", d.Id(), err)
   880  		}
   881  
   882  		// Refresh Backends
   883  		log.Printf("[DEBUG] Refreshing Backends for (%s)", d.Id())
   884  		backendList, err := conn.ListBackends(&gofastly.ListBackendsInput{
   885  			Service: d.Id(),
   886  			Version: s.ActiveVersion.Number,
   887  		})
   888  
   889  		if err != nil {
   890  			return fmt.Errorf("[ERR] Error looking up Backends for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
   891  		}
   892  
   893  		bl := flattenBackends(backendList)
   894  
   895  		if err := d.Set("backend", bl); err != nil {
   896  			log.Printf("[WARN] Error setting Backends for (%s): %s", d.Id(), err)
   897  		}
   898  
   899  		// refresh headers
   900  		log.Printf("[DEBUG] Refreshing Headers for (%s)", d.Id())
   901  		headerList, err := conn.ListHeaders(&gofastly.ListHeadersInput{
   902  			Service: d.Id(),
   903  			Version: s.ActiveVersion.Number,
   904  		})
   905  
   906  		if err != nil {
   907  			return fmt.Errorf("[ERR] Error looking up Headers for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
   908  		}
   909  
   910  		hl := flattenHeaders(headerList)
   911  
   912  		if err := d.Set("header", hl); err != nil {
   913  			log.Printf("[WARN] Error setting Headers for (%s): %s", d.Id(), err)
   914  		}
   915  
   916  		// refresh gzips
   917  		log.Printf("[DEBUG] Refreshing Gzips for (%s)", d.Id())
   918  		gzipsList, err := conn.ListGzips(&gofastly.ListGzipsInput{
   919  			Service: d.Id(),
   920  			Version: s.ActiveVersion.Number,
   921  		})
   922  
   923  		if err != nil {
   924  			return fmt.Errorf("[ERR] Error looking up Gzips for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
   925  		}
   926  
   927  		gl := flattenGzips(gzipsList)
   928  
   929  		if err := d.Set("gzip", gl); err != nil {
   930  			log.Printf("[WARN] Error setting Gzips for (%s): %s", d.Id(), err)
   931  		}
   932  
   933  		// refresh S3 Logging
   934  		log.Printf("[DEBUG] Refreshing S3 Logging for (%s)", d.Id())
   935  		s3List, err := conn.ListS3s(&gofastly.ListS3sInput{
   936  			Service: d.Id(),
   937  			Version: s.ActiveVersion.Number,
   938  		})
   939  
   940  		if err != nil {
   941  			return fmt.Errorf("[ERR] Error looking up S3 Logging for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
   942  		}
   943  
   944  		sl := flattenS3s(s3List)
   945  
   946  		if err := d.Set("s3logging", sl); err != nil {
   947  			log.Printf("[WARN] Error setting S3 Logging for (%s): %s", d.Id(), err)
   948  		}
   949  
   950  	} else {
   951  		log.Printf("[DEBUG] Active Version for Service (%s) is empty, no state to refresh", d.Id())
   952  	}
   953  
   954  	return nil
   955  }
   956  
   957  func resourceServiceV1Delete(d *schema.ResourceData, meta interface{}) error {
   958  	conn := meta.(*FastlyClient).conn
   959  
   960  	// Fastly will fail to delete any service with an Active Version.
   961  	// If `force_destroy` is given, we deactivate the active version and then send
   962  	// the DELETE call
   963  	if d.Get("force_destroy").(bool) {
   964  		s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{
   965  			ID: d.Id(),
   966  		})
   967  
   968  		if err != nil {
   969  			return err
   970  		}
   971  
   972  		if s.ActiveVersion.Number != "" {
   973  			_, err := conn.DeactivateVersion(&gofastly.DeactivateVersionInput{
   974  				Service: d.Id(),
   975  				Version: s.ActiveVersion.Number,
   976  			})
   977  			if err != nil {
   978  				return err
   979  			}
   980  		}
   981  	}
   982  
   983  	err := conn.DeleteService(&gofastly.DeleteServiceInput{
   984  		ID: d.Id(),
   985  	})
   986  
   987  	if err != nil {
   988  		return err
   989  	}
   990  
   991  	_, err = findService(d.Id(), meta)
   992  	if err != nil {
   993  		switch err {
   994  		// we expect no records to be found here
   995  		case fastlyNoServiceFoundErr:
   996  			d.SetId("")
   997  			return nil
   998  		default:
   999  			return err
  1000  		}
  1001  	}
  1002  
  1003  	// findService above returned something and nil error, but shouldn't have
  1004  	return fmt.Errorf("[WARN] Tried deleting Service (%s), but was still found", d.Id())
  1005  
  1006  }
  1007  
  1008  func flattenDomains(list []*gofastly.Domain) []map[string]interface{} {
  1009  	dl := make([]map[string]interface{}, 0, len(list))
  1010  
  1011  	for _, d := range list {
  1012  		dl = append(dl, map[string]interface{}{
  1013  			"name":    d.Name,
  1014  			"comment": d.Comment,
  1015  		})
  1016  	}
  1017  
  1018  	return dl
  1019  }
  1020  
  1021  func flattenBackends(backendList []*gofastly.Backend) []map[string]interface{} {
  1022  	var bl []map[string]interface{}
  1023  	for _, b := range backendList {
  1024  		// Convert Backend to a map for saving to state.
  1025  		nb := map[string]interface{}{
  1026  			"name":                  b.Name,
  1027  			"address":               b.Address,
  1028  			"auto_loadbalance":      b.AutoLoadbalance,
  1029  			"between_bytes_timeout": int(b.BetweenBytesTimeout),
  1030  			"connect_timeout":       int(b.ConnectTimeout),
  1031  			"error_threshold":       int(b.ErrorThreshold),
  1032  			"first_byte_timeout":    int(b.FirstByteTimeout),
  1033  			"max_conn":              int(b.MaxConn),
  1034  			"port":                  int(b.Port),
  1035  			"ssl_check_cert":        b.SSLCheckCert,
  1036  			"weight":                int(b.Weight),
  1037  		}
  1038  
  1039  		bl = append(bl, nb)
  1040  	}
  1041  	return bl
  1042  }
  1043  
  1044  // findService finds a Fastly Service via the ListServices endpoint, returning
  1045  // the Service if found.
  1046  //
  1047  // Fastly API does not include any "deleted_at" type parameter to indicate
  1048  // that a Service has been deleted. GET requests to a deleted Service will
  1049  // return 200 OK and have the full output of the Service for an unknown time
  1050  // (days, in my testing). In order to determine if a Service is deleted, we
  1051  // need to hit /service and loop the returned Services, searching for the one
  1052  // in question. This endpoint only returns active or "alive" services. If the
  1053  // Service is not included, then it's "gone"
  1054  //
  1055  // Returns a fastlyNoServiceFoundErr error if the Service is not found in the
  1056  // ListServices response.
  1057  func findService(id string, meta interface{}) (*gofastly.Service, error) {
  1058  	conn := meta.(*FastlyClient).conn
  1059  
  1060  	l, err := conn.ListServices(&gofastly.ListServicesInput{})
  1061  	if err != nil {
  1062  		return nil, fmt.Errorf("[WARN] Error listing services when deleting Fastly Service (%s): %s", id, err)
  1063  	}
  1064  
  1065  	for _, s := range l {
  1066  		if s.ID == id {
  1067  			log.Printf("[DEBUG] Found Service (%s)", id)
  1068  			return s, nil
  1069  		}
  1070  	}
  1071  
  1072  	return nil, fastlyNoServiceFoundErr
  1073  }
  1074  
  1075  func flattenHeaders(headerList []*gofastly.Header) []map[string]interface{} {
  1076  	var hl []map[string]interface{}
  1077  	for _, h := range headerList {
  1078  		// Convert Header to a map for saving to state.
  1079  		nh := map[string]interface{}{
  1080  			"name":               h.Name,
  1081  			"action":             h.Action,
  1082  			"ignore_if_set":      h.IgnoreIfSet,
  1083  			"type":               h.Type,
  1084  			"destination":        h.Destination,
  1085  			"source":             h.Source,
  1086  			"regex":              h.Regex,
  1087  			"substitution":       h.Substitution,
  1088  			"priority":           int(h.Priority),
  1089  			"request_condition":  h.RequestCondition,
  1090  			"cache_condition":    h.CacheCondition,
  1091  			"response_condition": h.ResponseCondition,
  1092  		}
  1093  
  1094  		for k, v := range nh {
  1095  			if v == "" {
  1096  				delete(nh, k)
  1097  			}
  1098  		}
  1099  
  1100  		hl = append(hl, nh)
  1101  	}
  1102  	return hl
  1103  }
  1104  
  1105  func buildHeader(headerMap interface{}) (*gofastly.CreateHeaderInput, error) {
  1106  	df := headerMap.(map[string]interface{})
  1107  	opts := gofastly.CreateHeaderInput{
  1108  		Name:              df["name"].(string),
  1109  		IgnoreIfSet:       df["ignore_if_set"].(bool),
  1110  		Destination:       df["destination"].(string),
  1111  		Priority:          uint(df["priority"].(int)),
  1112  		Source:            df["source"].(string),
  1113  		Regex:             df["regex"].(string),
  1114  		Substitution:      df["substitution"].(string),
  1115  		RequestCondition:  df["request_condition"].(string),
  1116  		CacheCondition:    df["cache_condition"].(string),
  1117  		ResponseCondition: df["response_condition"].(string),
  1118  	}
  1119  
  1120  	act := strings.ToLower(df["action"].(string))
  1121  	switch act {
  1122  	case "set":
  1123  		opts.Action = gofastly.HeaderActionSet
  1124  	case "append":
  1125  		opts.Action = gofastly.HeaderActionAppend
  1126  	case "delete":
  1127  		opts.Action = gofastly.HeaderActionDelete
  1128  	case "regex":
  1129  		opts.Action = gofastly.HeaderActionRegex
  1130  	case "regex_repeat":
  1131  		opts.Action = gofastly.HeaderActionRegexRepeat
  1132  	}
  1133  
  1134  	ty := strings.ToLower(df["type"].(string))
  1135  	switch ty {
  1136  	case "request":
  1137  		opts.Type = gofastly.HeaderTypeRequest
  1138  	case "fetch":
  1139  		opts.Type = gofastly.HeaderTypeFetch
  1140  	case "cache":
  1141  		opts.Type = gofastly.HeaderTypeCache
  1142  	case "response":
  1143  		opts.Type = gofastly.HeaderTypeResponse
  1144  	}
  1145  
  1146  	return &opts, nil
  1147  }
  1148  
  1149  func flattenGzips(gzipsList []*gofastly.Gzip) []map[string]interface{} {
  1150  	var gl []map[string]interface{}
  1151  	for _, g := range gzipsList {
  1152  		// Convert Gzip to a map for saving to state.
  1153  		ng := map[string]interface{}{
  1154  			"name":            g.Name,
  1155  			"cache_condition": g.CacheCondition,
  1156  		}
  1157  
  1158  		if g.Extensions != "" {
  1159  			e := strings.Split(g.Extensions, " ")
  1160  			var et []interface{}
  1161  			for _, ev := range e {
  1162  				et = append(et, ev)
  1163  			}
  1164  			ng["extensions"] = schema.NewSet(schema.HashString, et)
  1165  		}
  1166  
  1167  		if g.ContentTypes != "" {
  1168  			c := strings.Split(g.ContentTypes, " ")
  1169  			var ct []interface{}
  1170  			for _, cv := range c {
  1171  				ct = append(ct, cv)
  1172  			}
  1173  			ng["content_types"] = schema.NewSet(schema.HashString, ct)
  1174  		}
  1175  
  1176  		// prune any empty values that come from the default string value in structs
  1177  		for k, v := range ng {
  1178  			if v == "" {
  1179  				delete(ng, k)
  1180  			}
  1181  		}
  1182  
  1183  		gl = append(gl, ng)
  1184  	}
  1185  
  1186  	return gl
  1187  }
  1188  
  1189  func flattenS3s(s3List []*gofastly.S3) []map[string]interface{} {
  1190  	var sl []map[string]interface{}
  1191  	for _, s := range s3List {
  1192  		// Convert S3s to a map for saving to state.
  1193  		ns := map[string]interface{}{
  1194  			"name":             s.Name,
  1195  			"bucket_name":      s.BucketName,
  1196  			"s3_access_key":    s.AccessKey,
  1197  			"s3_secret_key":    s.SecretKey,
  1198  			"path":             s.Path,
  1199  			"period":           s.Period,
  1200  			"domain":           s.Domain,
  1201  			"gzip_level":       s.GzipLevel,
  1202  			"format":           s.Format,
  1203  			"timestamp_format": s.TimestampFormat,
  1204  		}
  1205  
  1206  		// prune any empty values that come from the default string value in structs
  1207  		for k, v := range ns {
  1208  			if v == "" {
  1209  				delete(ns, k)
  1210  			}
  1211  		}
  1212  
  1213  		sl = append(sl, ns)
  1214  	}
  1215  
  1216  	return sl
  1217  }