github.com/andresvia/terraform@v0.6.15-0.20160412045437-d51c75946785/builtin/providers/fastly/resource_fastly_service_v1.go (about)

     1  package fastly
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"time"
     8  
     9  	"github.com/hashicorp/terraform/helper/schema"
    10  	gofastly "github.com/sethvargo/go-fastly"
    11  )
    12  
    13  var fastlyNoServiceFoundErr = errors.New("No matching Fastly Service found")
    14  
    15  func resourceServiceV1() *schema.Resource {
    16  	return &schema.Resource{
    17  		Create: resourceServiceV1Create,
    18  		Read:   resourceServiceV1Read,
    19  		Update: resourceServiceV1Update,
    20  		Delete: resourceServiceV1Delete,
    21  
    22  		Schema: map[string]*schema.Schema{
    23  			"name": &schema.Schema{
    24  				Type:        schema.TypeString,
    25  				Required:    true,
    26  				Description: "Unique name for this Service",
    27  			},
    28  
    29  			// Active Version represents the currently activated version in Fastly. In
    30  			// Terraform, we abstract this number away from the users and manage
    31  			// creating and activating. It's used internally, but also exported for
    32  			// users to see.
    33  			"active_version": &schema.Schema{
    34  				Type:     schema.TypeString,
    35  				Computed: true,
    36  			},
    37  
    38  			"domain": &schema.Schema{
    39  				Type:     schema.TypeSet,
    40  				Required: true,
    41  				Elem: &schema.Resource{
    42  					Schema: map[string]*schema.Schema{
    43  						"name": &schema.Schema{
    44  							Type:        schema.TypeString,
    45  							Required:    true,
    46  							Description: "The domain that this Service will respond to",
    47  						},
    48  
    49  						"comment": &schema.Schema{
    50  							Type:     schema.TypeString,
    51  							Optional: true,
    52  						},
    53  					},
    54  				},
    55  			},
    56  
    57  			"default_ttl": &schema.Schema{
    58  				Type:        schema.TypeInt,
    59  				Optional:    true,
    60  				Default:     3600,
    61  				Description: "The default Time-to-live (TTL) for the version",
    62  			},
    63  
    64  			"default_host": &schema.Schema{
    65  				Type:        schema.TypeString,
    66  				Optional:    true,
    67  				Computed:    true,
    68  				Description: "The default hostname for the version",
    69  			},
    70  
    71  			"backend": &schema.Schema{
    72  				Type:     schema.TypeSet,
    73  				Required: true,
    74  				Elem: &schema.Resource{
    75  					Schema: map[string]*schema.Schema{
    76  						// required fields
    77  						"name": &schema.Schema{
    78  							Type:        schema.TypeString,
    79  							Required:    true,
    80  							Description: "A name for this Backend",
    81  						},
    82  						"address": &schema.Schema{
    83  							Type:        schema.TypeString,
    84  							Required:    true,
    85  							Description: "An IPv4, hostname, or IPv6 address for the Backend",
    86  						},
    87  						// Optional fields, defaults where they exist
    88  						"auto_loadbalance": &schema.Schema{
    89  							Type:        schema.TypeBool,
    90  							Optional:    true,
    91  							Default:     true,
    92  							Description: "Should this Backend be load balanced",
    93  						},
    94  						"between_bytes_timeout": &schema.Schema{
    95  							Type:        schema.TypeInt,
    96  							Optional:    true,
    97  							Default:     10000,
    98  							Description: "How long to wait between bytes in milliseconds",
    99  						},
   100  						"connect_timeout": &schema.Schema{
   101  							Type:        schema.TypeInt,
   102  							Optional:    true,
   103  							Default:     1000,
   104  							Description: "How long to wait for a timeout in milliseconds",
   105  						},
   106  						"error_threshold": &schema.Schema{
   107  							Type:        schema.TypeInt,
   108  							Optional:    true,
   109  							Default:     0,
   110  							Description: "Number of errors to allow before the Backend is marked as down",
   111  						},
   112  						"first_byte_timeout": &schema.Schema{
   113  							Type:        schema.TypeInt,
   114  							Optional:    true,
   115  							Default:     15000,
   116  							Description: "How long to wait for the first bytes in milliseconds",
   117  						},
   118  						"max_conn": &schema.Schema{
   119  							Type:        schema.TypeInt,
   120  							Optional:    true,
   121  							Default:     200,
   122  							Description: "Maximum number of connections for this Backend",
   123  						},
   124  						"port": &schema.Schema{
   125  							Type:        schema.TypeInt,
   126  							Optional:    true,
   127  							Default:     80,
   128  							Description: "The port number Backend responds on. Default 80",
   129  						},
   130  						"ssl_check_cert": &schema.Schema{
   131  							Type:        schema.TypeBool,
   132  							Optional:    true,
   133  							Default:     true,
   134  							Description: "Be strict on checking SSL certs",
   135  						},
   136  						// UseSSL is something we want to support in the future, but
   137  						// requires SSL setup we don't yet have
   138  						// TODO: Provide all SSL fields from https://docs.fastly.com/api/config#backend
   139  						// "use_ssl": &schema.Schema{
   140  						// 	Type:        schema.TypeBool,
   141  						// 	Optional:    true,
   142  						// 	Default:     false,
   143  						// 	Description: "Whether or not to use SSL to reach the Backend",
   144  						// },
   145  						"weight": &schema.Schema{
   146  							Type:        schema.TypeInt,
   147  							Optional:    true,
   148  							Default:     100,
   149  							Description: "How long to wait for the first bytes in milliseconds",
   150  						},
   151  					},
   152  				},
   153  			},
   154  
   155  			"force_destroy": &schema.Schema{
   156  				Type:     schema.TypeBool,
   157  				Optional: true,
   158  			},
   159  		},
   160  	}
   161  }
   162  
   163  func resourceServiceV1Create(d *schema.ResourceData, meta interface{}) error {
   164  	conn := meta.(*FastlyClient).conn
   165  	service, err := conn.CreateService(&gofastly.CreateServiceInput{
   166  		Name:    d.Get("name").(string),
   167  		Comment: "Managed by Terraform",
   168  	})
   169  
   170  	if err != nil {
   171  		return err
   172  	}
   173  
   174  	d.SetId(service.ID)
   175  	return resourceServiceV1Update(d, meta)
   176  }
   177  
   178  func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
   179  	conn := meta.(*FastlyClient).conn
   180  
   181  	// Update Name. No new verions is required for this
   182  	if d.HasChange("name") {
   183  		_, err := conn.UpdateService(&gofastly.UpdateServiceInput{
   184  			ID:   d.Id(),
   185  			Name: d.Get("name").(string),
   186  		})
   187  		if err != nil {
   188  			return err
   189  		}
   190  	}
   191  
   192  	// Once activated, Versions are locked and become immutable. This is true for
   193  	// versions that are no longer active. For Domains, Backends, DefaultHost and
   194  	// DefaultTTL, a new Version must be created first, and updates posted to that
   195  	// Version. Loop these attributes and determine if we need to create a new version first
   196  	var needsChange bool
   197  	for _, v := range []string{"domain", "backend", "default_host", "default_ttl"} {
   198  		if d.HasChange(v) {
   199  			needsChange = true
   200  		}
   201  	}
   202  
   203  	if needsChange {
   204  		latestVersion := d.Get("active_version").(string)
   205  		if latestVersion == "" {
   206  			// If the service was just created, there is an empty Version 1 available
   207  			// that is unlocked and can be updated
   208  			latestVersion = "1"
   209  		} else {
   210  			// Clone the latest version, giving us an unlocked version we can modify
   211  			log.Printf("[DEBUG] Creating clone of version (%s) for updates", latestVersion)
   212  			newVersion, err := conn.CloneVersion(&gofastly.CloneVersionInput{
   213  				Service: d.Id(),
   214  				Version: latestVersion,
   215  			})
   216  			if err != nil {
   217  				return err
   218  			}
   219  
   220  			// The new version number is named "Number", but it's actually a string
   221  			latestVersion = newVersion.Number
   222  
   223  			// New versions are not immediately found in the API, or are not
   224  			// immediately mutable, so we need to sleep a few and let Fastly ready
   225  			// itself. Typically, 7 seconds is enough
   226  			log.Printf("[DEBUG] Sleeping 7 seconds to allow Fastly Version to be available")
   227  			time.Sleep(7 * time.Second)
   228  		}
   229  
   230  		// update general settings
   231  		if d.HasChange("default_host") || d.HasChange("default_ttl") {
   232  			opts := gofastly.UpdateSettingsInput{
   233  				Service: d.Id(),
   234  				Version: latestVersion,
   235  				// default_ttl has the same default value of 3600 that is provided by
   236  				// the Fastly API, so it's safe to include here
   237  				DefaultTTL: uint(d.Get("default_ttl").(int)),
   238  			}
   239  
   240  			if attr, ok := d.GetOk("default_host"); ok {
   241  				opts.DefaultHost = attr.(string)
   242  			}
   243  
   244  			log.Printf("[DEBUG] Update Settings opts: %#v", opts)
   245  			_, err := conn.UpdateSettings(&opts)
   246  			if err != nil {
   247  				return err
   248  			}
   249  		}
   250  
   251  		// Find differences in domains
   252  		if d.HasChange("domain") {
   253  			// Note: we don't utilize the PUT endpoint to update a Domain, we simply
   254  			// destroy it and create a new one. This is how Terraform works with nested
   255  			// sub resources, we only get the full diff not a partial set item diff.
   256  			// Because this is done on a new version of the configuration, this is
   257  			// considered safe
   258  			od, nd := d.GetChange("domain")
   259  			if od == nil {
   260  				od = new(schema.Set)
   261  			}
   262  			if nd == nil {
   263  				nd = new(schema.Set)
   264  			}
   265  
   266  			ods := od.(*schema.Set)
   267  			nds := nd.(*schema.Set)
   268  
   269  			remove := ods.Difference(nds).List()
   270  			add := nds.Difference(ods).List()
   271  
   272  			// Delete removed domains
   273  			for _, dRaw := range remove {
   274  				df := dRaw.(map[string]interface{})
   275  				opts := gofastly.DeleteDomainInput{
   276  					Service: d.Id(),
   277  					Version: latestVersion,
   278  					Name:    df["name"].(string),
   279  				}
   280  
   281  				log.Printf("[DEBUG] Fastly Domain Removal opts: %#v", opts)
   282  				err := conn.DeleteDomain(&opts)
   283  				if err != nil {
   284  					return err
   285  				}
   286  			}
   287  
   288  			// POST new Domains
   289  			for _, dRaw := range add {
   290  				df := dRaw.(map[string]interface{})
   291  				opts := gofastly.CreateDomainInput{
   292  					Service: d.Id(),
   293  					Version: latestVersion,
   294  					Name:    df["name"].(string),
   295  				}
   296  
   297  				if v, ok := df["comment"]; ok {
   298  					opts.Comment = v.(string)
   299  				}
   300  
   301  				log.Printf("[DEBUG] Fastly Domain Addition opts: %#v", opts)
   302  				_, err := conn.CreateDomain(&opts)
   303  				if err != nil {
   304  					return err
   305  				}
   306  			}
   307  		}
   308  
   309  		// find difference in backends
   310  		if d.HasChange("backend") {
   311  			// POST new Backends
   312  			// Note: we don't utilize the PUT endpoint to update a Backend, we simply
   313  			// destroy it and create a new one. This is how Terraform works with nested
   314  			// sub resources, we only get the full diff not a partial set item diff.
   315  			// Because this is done on a new version of the configuration, this is
   316  			// considered safe
   317  			ob, nb := d.GetChange("backend")
   318  			if ob == nil {
   319  				ob = new(schema.Set)
   320  			}
   321  			if nb == nil {
   322  				nb = new(schema.Set)
   323  			}
   324  
   325  			obs := ob.(*schema.Set)
   326  			nbs := nb.(*schema.Set)
   327  			removeBackends := obs.Difference(nbs).List()
   328  			addBackends := nbs.Difference(obs).List()
   329  
   330  			// DELETE old Backends
   331  			for _, bRaw := range removeBackends {
   332  				bf := bRaw.(map[string]interface{})
   333  				opts := gofastly.DeleteBackendInput{
   334  					Service: d.Id(),
   335  					Version: latestVersion,
   336  					Name:    bf["name"].(string),
   337  				}
   338  
   339  				log.Printf("[DEBUG] Fastly Backend Removal opts: %#v", opts)
   340  				err := conn.DeleteBackend(&opts)
   341  				if err != nil {
   342  					return err
   343  				}
   344  			}
   345  
   346  			for _, dRaw := range addBackends {
   347  				df := dRaw.(map[string]interface{})
   348  				opts := gofastly.CreateBackendInput{
   349  					Service:             d.Id(),
   350  					Version:             latestVersion,
   351  					Name:                df["name"].(string),
   352  					Address:             df["address"].(string),
   353  					AutoLoadbalance:     df["auto_loadbalance"].(bool),
   354  					SSLCheckCert:        df["ssl_check_cert"].(bool),
   355  					Port:                uint(df["port"].(int)),
   356  					BetweenBytesTimeout: uint(df["between_bytes_timeout"].(int)),
   357  					ConnectTimeout:      uint(df["connect_timeout"].(int)),
   358  					ErrorThreshold:      uint(df["error_threshold"].(int)),
   359  					FirstByteTimeout:    uint(df["first_byte_timeout"].(int)),
   360  					MaxConn:             uint(df["max_conn"].(int)),
   361  					Weight:              uint(df["weight"].(int)),
   362  				}
   363  
   364  				log.Printf("[DEBUG] Create Backend Opts: %#v", opts)
   365  				_, err := conn.CreateBackend(&opts)
   366  				if err != nil {
   367  					return err
   368  				}
   369  			}
   370  		}
   371  
   372  		// validate version
   373  		log.Printf("[DEBUG] Validating Fastly Service (%s), Version (%s)", d.Id(), latestVersion)
   374  		valid, msg, err := conn.ValidateVersion(&gofastly.ValidateVersionInput{
   375  			Service: d.Id(),
   376  			Version: latestVersion,
   377  		})
   378  
   379  		if err != nil {
   380  			return fmt.Errorf("[ERR] Error checking validation: %s", err)
   381  		}
   382  
   383  		if !valid {
   384  			return fmt.Errorf("[ERR] Invalid configuration for Fastly Service (%s): %s", d.Id(), msg)
   385  		}
   386  
   387  		log.Printf("[DEBUG] Activating Fastly Service (%s), Version (%s)", d.Id(), latestVersion)
   388  		_, err = conn.ActivateVersion(&gofastly.ActivateVersionInput{
   389  			Service: d.Id(),
   390  			Version: latestVersion,
   391  		})
   392  		if err != nil {
   393  			return fmt.Errorf("[ERR] Error activating version (%s): %s", latestVersion, err)
   394  		}
   395  
   396  		// Only if the version is valid and activated do we set the active_version.
   397  		// This prevents us from getting stuck in cloning an invalid version
   398  		d.Set("active_version", latestVersion)
   399  	}
   400  
   401  	return resourceServiceV1Read(d, meta)
   402  }
   403  
   404  func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error {
   405  	conn := meta.(*FastlyClient).conn
   406  
   407  	// Find the Service. Discard the service because we need the ServiceDetails,
   408  	// not just a Service record
   409  	_, err := findService(d.Id(), meta)
   410  	if err != nil {
   411  		switch err {
   412  		case fastlyNoServiceFoundErr:
   413  			log.Printf("[WARN] %s for ID (%s)", err, d.Id())
   414  			d.SetId("")
   415  			return nil
   416  		default:
   417  			return err
   418  		}
   419  	}
   420  
   421  	s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{
   422  		ID: d.Id(),
   423  	})
   424  
   425  	if err != nil {
   426  		return err
   427  	}
   428  
   429  	d.Set("name", s.Name)
   430  	d.Set("active_version", s.ActiveVersion.Number)
   431  
   432  	// If CreateService succeeds, but initial updates to the Service fail, we'll
   433  	// have an empty ActiveService version (no version is active, so we can't
   434  	// query for information on it)
   435  	if s.ActiveVersion.Number != "" {
   436  		settingsOpts := gofastly.GetSettingsInput{
   437  			Service: d.Id(),
   438  			Version: s.ActiveVersion.Number,
   439  		}
   440  		if settings, err := conn.GetSettings(&settingsOpts); err == nil {
   441  			d.Set("default_host", settings.DefaultHost)
   442  			d.Set("default_ttl", settings.DefaultTTL)
   443  		} else {
   444  			return fmt.Errorf("[ERR] Error looking up Version settings for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
   445  		}
   446  
   447  		// TODO: update go-fastly to support an ActiveVersion struct, which contains
   448  		// domain and backend info in the response. Here we do 2 additional queries
   449  		// to find out that info
   450  		domainList, err := conn.ListDomains(&gofastly.ListDomainsInput{
   451  			Service: d.Id(),
   452  			Version: s.ActiveVersion.Number,
   453  		})
   454  
   455  		if err != nil {
   456  			return fmt.Errorf("[ERR] Error looking up Domains for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
   457  		}
   458  
   459  		// Refresh Domains
   460  		dl := flattenDomains(domainList)
   461  
   462  		if err := d.Set("domain", dl); err != nil {
   463  			log.Printf("[WARN] Error setting Domains for (%s): %s", d.Id(), err)
   464  		}
   465  
   466  		// Refresh Backends
   467  		backendList, err := conn.ListBackends(&gofastly.ListBackendsInput{
   468  			Service: d.Id(),
   469  			Version: s.ActiveVersion.Number,
   470  		})
   471  
   472  		if err != nil {
   473  			return fmt.Errorf("[ERR] Error looking up Backends for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
   474  		}
   475  
   476  		bl := flattenBackends(backendList)
   477  
   478  		if err := d.Set("backend", bl); err != nil {
   479  			log.Printf("[WARN] Error setting Backends for (%s): %s", d.Id(), err)
   480  		}
   481  	} else {
   482  		log.Printf("[DEBUG] Active Version for Service (%s) is empty, no state to refresh", d.Id())
   483  	}
   484  
   485  	return nil
   486  }
   487  
   488  func resourceServiceV1Delete(d *schema.ResourceData, meta interface{}) error {
   489  	conn := meta.(*FastlyClient).conn
   490  
   491  	// Fastly will fail to delete any service with an Active Version.
   492  	// If `force_destroy` is given, we deactivate the active version and then send
   493  	// the DELETE call
   494  	if d.Get("force_destroy").(bool) {
   495  		s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{
   496  			ID: d.Id(),
   497  		})
   498  
   499  		if err != nil {
   500  			return err
   501  		}
   502  
   503  		if s.ActiveVersion.Number != "" {
   504  			_, err := conn.DeactivateVersion(&gofastly.DeactivateVersionInput{
   505  				Service: d.Id(),
   506  				Version: s.ActiveVersion.Number,
   507  			})
   508  			if err != nil {
   509  				return err
   510  			}
   511  		}
   512  	}
   513  
   514  	err := conn.DeleteService(&gofastly.DeleteServiceInput{
   515  		ID: d.Id(),
   516  	})
   517  
   518  	if err != nil {
   519  		return err
   520  	}
   521  
   522  	_, err = findService(d.Id(), meta)
   523  	if err != nil {
   524  		switch err {
   525  		// we expect no records to be found here
   526  		case fastlyNoServiceFoundErr:
   527  			d.SetId("")
   528  			return nil
   529  		default:
   530  			return err
   531  		}
   532  	}
   533  
   534  	// findService above returned something and nil error, but shouldn't have
   535  	return fmt.Errorf("[WARN] Tried deleting Service (%s), but was still found", d.Id())
   536  
   537  }
   538  
   539  func flattenDomains(list []*gofastly.Domain) []map[string]interface{} {
   540  	dl := make([]map[string]interface{}, 0, len(list))
   541  
   542  	for _, d := range list {
   543  		dl = append(dl, map[string]interface{}{
   544  			"name":    d.Name,
   545  			"comment": d.Comment,
   546  		})
   547  	}
   548  
   549  	return dl
   550  }
   551  
   552  func flattenBackends(backendList []*gofastly.Backend) []map[string]interface{} {
   553  	var bl []map[string]interface{}
   554  	for _, b := range backendList {
   555  		// Convert Backend to a map for saving to state.
   556  		nb := map[string]interface{}{
   557  			"name":                  b.Name,
   558  			"address":               b.Address,
   559  			"auto_loadbalance":      b.AutoLoadbalance,
   560  			"between_bytes_timeout": int(b.BetweenBytesTimeout),
   561  			"connect_timeout":       int(b.ConnectTimeout),
   562  			"error_threshold":       int(b.ErrorThreshold),
   563  			"first_byte_timeout":    int(b.FirstByteTimeout),
   564  			"max_conn":              int(b.MaxConn),
   565  			"port":                  int(b.Port),
   566  			"ssl_check_cert":        b.SSLCheckCert,
   567  			"weight":                int(b.Weight),
   568  		}
   569  
   570  		bl = append(bl, nb)
   571  	}
   572  	return bl
   573  }
   574  
   575  // findService finds a Fastly Service via the ListServices endpoint, returning
   576  // the Service if found.
   577  //
   578  // Fastly API does not include any "deleted_at" type parameter to indicate
   579  // that a Service has been deleted. GET requests to a deleted Service will
   580  // return 200 OK and have the full output of the Service for an unknown time
   581  // (days, in my testing). In order to determine if a Service is deleted, we
   582  // need to hit /service and loop the returned Services, searching for the one
   583  // in question. This endpoint only returns active or "alive" services. If the
   584  // Service is not included, then it's "gone"
   585  //
   586  // Returns a fastlyNoServiceFoundErr error if the Service is not found in the
   587  // ListServices response.
   588  func findService(id string, meta interface{}) (*gofastly.Service, error) {
   589  	conn := meta.(*FastlyClient).conn
   590  
   591  	l, err := conn.ListServices(&gofastly.ListServicesInput{})
   592  	if err != nil {
   593  		return nil, fmt.Errorf("[WARN] Error listing servcies when deleting Fastly Service (%s): %s", id, err)
   594  	}
   595  
   596  	for _, s := range l {
   597  		if s.ID == id {
   598  			log.Printf("[DEBUG] Found Service (%s)", id)
   599  			return s, nil
   600  		}
   601  	}
   602  
   603  	return nil, fastlyNoServiceFoundErr
   604  }