provider/fastly: Add Fastly Provider, ServiceV1 resource

This commit is contained in:
clint shryock 2016-03-23 14:53:50 -05:00
parent 2cc8adefb8
commit 2ad37bba4a
12 changed files with 1374 additions and 0 deletions

View File

@ -0,0 +1,12 @@
package main
import (
"github.com/hashicorp/terraform/builtin/providers/fastly"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: fastly.Provider,
})
}

View File

@ -0,0 +1 @@
package main

View File

@ -0,0 +1,31 @@
package fastly
import (
"fmt"
gofastly "github.com/sethvargo/go-fastly"
)
type Config struct {
ApiKey string
}
type FastlyClient struct {
conn *gofastly.Client
}
func (c *Config) Client() (interface{}, error) {
var client FastlyClient
if c.ApiKey == "" {
return nil, fmt.Errorf("[Err] No API key for Fastly")
}
fconn, err := gofastly.NewClient(c.ApiKey)
if err != nil {
return nil, err
}
client.conn = fconn
return &client, nil
}

View File

@ -0,0 +1,34 @@
package fastly
import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
// Provider returns a terraform.ResourceProvider.
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"api_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{
"FASTLY_API_KEY",
}, nil),
Description: "Fastly API Key from https://app.fastly.com/#account",
},
},
ResourcesMap: map[string]*schema.Resource{
"fastly_service_v1": resourceServiceV1(),
},
ConfigureFunc: providerConfigure,
}
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
ApiKey: d.Get("api_key").(string),
}
return config.Client()
}

View File

@ -0,0 +1,35 @@
package fastly
import (
"os"
"testing"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *schema.Provider
func init() {
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"fastly": testAccProvider,
}
}
func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = Provider()
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("FASTLY_API_KEY"); v == "" {
t.Fatal("FASTLY_API_KEY must be set for acceptance tests")
}
}

View File

@ -0,0 +1,603 @@
package fastly
import (
"errors"
"fmt"
"log"
"time"
"github.com/hashicorp/terraform/helper/schema"
gofastly "github.com/sethvargo/go-fastly"
)
var fastlyNoServiceFoundErr = errors.New("No matching Fastly Service found")
func resourceServiceV1() *schema.Resource {
return &schema.Resource{
Create: resourceServiceV1Create,
Read: resourceServiceV1Read,
Update: resourceServiceV1Update,
Delete: resourceServiceV1Delete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Unique name for this Service",
},
// Active Version represents the currently activated version in Fastly. In
// Terraform, we abstract this number away from the users and manage
// creating and activating. It's used internally, but also exported for
// users to see.
"active_version": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"domain": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "The domain that this Service will respond to",
},
"comment": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
},
"default_ttl": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 3600,
Description: "The default Time-to-live (TTL) for the version",
},
"default_host": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "The default hostname for the version",
},
"backend": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
// required fields
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "A name for this Backend",
},
"address": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "An IPv4, hostname, or IPv6 address for the Backend",
},
// Optional fields, defaults where they exist
"auto_loadbalance": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
Description: "Should this Backend be load balanced",
},
"between_bytes_timeout": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 10000,
Description: "How long to wait between bytes in milliseconds",
},
"connect_timeout": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 1000,
Description: "How long to wait for a timeout in milliseconds",
},
"error_threshold": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 0,
Description: "Number of errors to allow before the Backend is marked as down",
},
"first_byte_timeout": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 15000,
Description: "How long to wait for the first bytes in milliseconds",
},
"max_conn": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 200,
Description: "Maximum number of connections for this Backend",
},
"port": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 80,
Description: "The port number Backend responds on. Default 80",
},
"ssl_check_cert": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
Description: "Be strict on checking SSL certs",
},
// UseSSL is something we want to support in the future, but
// requires SSL setup we don't yet have
// TODO: Provide all SSL fields from https://docs.fastly.com/api/config#backend
// "use_ssl": &schema.Schema{
// Type: schema.TypeBool,
// Optional: true,
// Default: false,
// Description: "Whether or not to use SSL to reach the Backend",
// },
"weight": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 100,
Description: "How long to wait for the first bytes in milliseconds",
},
},
},
},
"force_destroy": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
},
}
}
func resourceServiceV1Create(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*FastlyClient).conn
service, err := conn.CreateService(&gofastly.CreateServiceInput{
Name: d.Get("name").(string),
Comment: "Managed by Terraform",
})
if err != nil {
return err
}
d.SetId(service.ID)
return resourceServiceV1Update(d, meta)
}
func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*FastlyClient).conn
// Update Name. No new verions is required for this
if d.HasChange("name") {
_, err := conn.UpdateService(&gofastly.UpdateServiceInput{
ID: d.Id(),
Name: d.Get("name").(string),
})
if err != nil {
return err
}
}
// Once activated, Versions are locked and become immutable. This is true for
// versions that are no longer active. For Domains, Backends, DefaultHost and
// DefaultTTL, a new Version must be created first, and updates posted to that
// Version. Loop these attributes and determine if we need to create a new version first
var needsChange bool
for _, v := range []string{"domain", "backend", "default_host", "default_ttl"} {
if d.HasChange(v) {
needsChange = true
}
}
if needsChange {
latestVersion := d.Get("active_version").(string)
if latestVersion == "" {
// If the service was just created, there is an empty Version 1 available
// that is unlocked and can be updated
latestVersion = "1"
} else {
// Clone the latest version, giving us an unlocked version we can modify
log.Printf("[DEBUG] Creating clone of version (%s) for updates", latestVersion)
newVersion, err := conn.CloneVersion(&gofastly.CloneVersionInput{
Service: d.Id(),
Version: latestVersion,
})
if err != nil {
return err
}
// The new version number is named "Number", but it's actually a string
latestVersion = newVersion.Number
// New versions are not immediately found in the API, or are not
// immediately mutable, so we need to sleep a few and let Fastly ready
// itself. Typically, 7 seconds is enough
time.Sleep(7 * time.Second)
}
// update general settings
if d.HasChange("default_host") || d.HasChange("default_ttl") {
opts := gofastly.UpdateSettingsInput{
Service: d.Id(),
Version: latestVersion,
// default_ttl has the same default value of 3600 that is provided by
// the Fastly API, so it's safe to include here
DefaultTTL: uint(d.Get("default_ttl").(int)),
}
if attr, ok := d.GetOk("default_host"); ok {
opts.DefaultHost = attr.(string)
}
log.Printf("[DEBUG] Update Settings opts: %#v", opts)
_, err := conn.UpdateSettings(&opts)
if err != nil {
return err
}
}
// Find differences in domains
if d.HasChange("domain") {
// Note: we don't utilize the PUT endpoint to update a Domain, we simply
// destroy it and create a new one. This is how Terraform works with nested
// sub resources, we only get the full diff not a partial set item diff.
// Because this is done on a new version of the configuration, this is
// considered safe
od, nd := d.GetChange("domain")
if od == nil {
od = new(schema.Set)
}
if nd == nil {
nd = new(schema.Set)
}
ods := od.(*schema.Set)
nds := nd.(*schema.Set)
remove := ods.Difference(nds).List()
add := nds.Difference(ods).List()
// Delete removed domains
for _, dRaw := range remove {
df := dRaw.(map[string]interface{})
opts := gofastly.DeleteDomainInput{
Service: d.Id(),
Version: latestVersion,
Name: df["name"].(string),
}
log.Printf("[DEBUG] Fastly Domain Removal opts: %#v", opts)
err := conn.DeleteDomain(&opts)
if err != nil {
return err
}
}
// POST new Domains
for _, dRaw := range add {
df := dRaw.(map[string]interface{})
opts := gofastly.CreateDomainInput{
Service: d.Id(),
Version: latestVersion,
Name: df["name"].(string),
}
if v, ok := df["comment"]; ok {
opts.Comment = v.(string)
}
log.Printf("[DEBUG] Fastly Domain Addition opts: %#v", opts)
_, err := conn.CreateDomain(&opts)
if err != nil {
return err
}
}
}
// find difference in backends
if d.HasChange("backend") {
// POST new Backends
// Note: we don't utilize the PUT endpoint to update a Backend, we simply
// destroy it and create a new one. This is how Terraform works with nested
// sub resources, we only get the full diff not a partial set item diff.
// Because this is done on a new version of the configuration, this is
// considered safe
ob, nb := d.GetChange("backend")
if ob == nil {
ob = new(schema.Set)
}
if nb == nil {
nb = new(schema.Set)
}
obs := ob.(*schema.Set)
nbs := nb.(*schema.Set)
removeBackends := obs.Difference(nbs).List()
addBackends := nbs.Difference(obs).List()
// DELETE old Backends
for _, bRaw := range removeBackends {
bf := bRaw.(map[string]interface{})
opts := gofastly.DeleteBackendInput{
Service: d.Id(),
Version: latestVersion,
Name: bf["name"].(string),
}
log.Printf("[DEBUG] Fastly Backend Removal opts: %#v", opts)
err := conn.DeleteBackend(&opts)
if err != nil {
return err
}
}
for _, dRaw := range addBackends {
df := dRaw.(map[string]interface{})
opts := gofastly.CreateBackendInput{
Service: d.Id(),
Version: latestVersion,
Name: df["name"].(string),
Address: df["address"].(string),
AutoLoadbalance: df["auto_loadbalance"].(bool),
SSLCheckCert: df["ssl_check_cert"].(bool),
Port: uint(df["port"].(int)),
BetweenBytesTimeout: uint(df["between_bytes_timeout"].(int)),
ConnectTimeout: uint(df["connect_timeout"].(int)),
ErrorThreshold: uint(df["error_threshold"].(int)),
FirstByteTimeout: uint(df["first_byte_timeout"].(int)),
MaxConn: uint(df["max_conn"].(int)),
Weight: uint(df["weight"].(int)),
}
log.Printf("[DEBUG] Create Backend Opts: %#v", opts)
_, err := conn.CreateBackend(&opts)
if err != nil {
return err
}
}
}
// validate version
log.Printf("[DEBUG] Validating Fastly Service (%s), Version (%s)", d.Id(), latestVersion)
valid, msg, err := conn.ValidateVersion(&gofastly.ValidateVersionInput{
Service: d.Id(),
Version: latestVersion,
})
if err != nil {
return fmt.Errorf("[ERR] Error checking validation: %s", err)
}
if !valid {
return fmt.Errorf("[WARN] Invalid configuration for Fastly Service (%s): %s", d.Id(), msg)
}
log.Printf("[DEBUG] Activating Fastly Service (%s), Version (%s)", d.Id(), latestVersion)
_, err = conn.ActivateVersion(&gofastly.ActivateVersionInput{
Service: d.Id(),
Version: latestVersion,
})
if err != nil {
return fmt.Errorf("[ERR] Error activating version (%s): %s", latestVersion, err)
}
// Only if the version is valid and activated do we set the active_version.
// This prevents us from getting stuck in cloning an invalid version
d.Set("active_version", latestVersion)
}
return resourceServiceV1Read(d, meta)
}
func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*FastlyClient).conn
// Find the Service. Discard the service because we need the ServiceDetails,
// not just a Service record
_, err := findService(d.Id(), meta)
if err != nil {
switch err {
case fastlyNoServiceFoundErr:
log.Printf("[WARN] %s for ID (%s)", err, d.Id())
d.SetId("")
return nil
default:
return err
}
}
s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{
ID: d.Id(),
})
if err != nil {
return err
}
d.Set("name", s.Name)
d.Set("active_version", s.ActiveVersion.Number)
// If CreateService succeeds, but initial updates to the Service fail, we'll
// have an empty ActiveService version (no version is active, so we can't
// query for information on it)
if s.ActiveVersion.Number != "" {
settingsOpts := gofastly.GetSettingsInput{
Service: d.Id(),
Version: s.ActiveVersion.Number,
}
if settings, err := conn.GetSettings(&settingsOpts); err == nil {
d.Set("default_host", settings.DefaultHost)
d.Set("default_ttl", settings.DefaultTTL)
} else {
return fmt.Errorf("[ERR] Error looking up Version settings for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
}
// TODO: update go-fastly to support an ActiveVersion struct, which contains
// domain and backend info in the response. Here we do 2 additional queries
// to find out that info
domainList, err := conn.ListDomains(&gofastly.ListDomainsInput{
Service: d.Id(),
Version: s.ActiveVersion.Number,
})
if err != nil {
return fmt.Errorf("[ERR] Error looking up Domains for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
}
// Refresh Domains
dl := flattenDomains(domainList)
if err := d.Set("domain", dl); err != nil {
log.Printf("[WARN] Error setting Domains for (%s): %s", d.Id(), err)
}
// Refresh Backends
backendList, err := conn.ListBackends(&gofastly.ListBackendsInput{
Service: d.Id(),
Version: s.ActiveVersion.Number,
})
if err != nil {
return fmt.Errorf("[ERR] Error looking up Backends for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
}
bl := flattenBackends(backendList)
if err := d.Set("backend", bl); err != nil {
log.Printf("[WARN] Error setting Backends for (%s): %s", d.Id(), err)
}
} else {
log.Printf("[DEBUG] Active Version for Service (%s) is empty, no state to refresh", d.Id())
}
return nil
}
func resourceServiceV1Delete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*FastlyClient).conn
// Fastly will fail to delete any service with an Active Version.
// If `force_destroy` is given, we deactivate the active version and then send
// the DELETE call
if d.Get("force_destroy").(bool) {
s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{
ID: d.Id(),
})
if err != nil {
return err
}
if s.ActiveVersion.Number != "" {
_, err := conn.DeactivateVersion(&gofastly.DeactivateVersionInput{
Service: d.Id(),
Version: s.ActiveVersion.Number,
})
if err != nil {
return err
}
}
}
err := conn.DeleteService(&gofastly.DeleteServiceInput{
ID: d.Id(),
})
if err != nil {
return err
}
_, err = findService(d.Id(), meta)
if err != nil {
switch err {
// we expect no records to be found here
case fastlyNoServiceFoundErr:
d.SetId("")
return nil
default:
return err
}
}
// findService above returned something and nil error, but shouldn't have
return fmt.Errorf("[WARN] Tried deleting Service (%s), but was still found", d.Id())
}
func flattenDomains(list []*gofastly.Domain) []map[string]interface{} {
dl := make([]map[string]interface{}, 0, len(list))
for _, d := range list {
dl = append(dl, map[string]interface{}{
"name": d.Name,
"comment": d.Comment,
})
}
return dl
}
func flattenBackends(backendList []*gofastly.Backend) []map[string]interface{} {
var bl []map[string]interface{}
for _, b := range backendList {
// Convert Backend to a map for saving to state.
nb := map[string]interface{}{
"name": b.Name,
"address": b.Address,
"auto_loadbalance": b.AutoLoadbalance,
"between_bytes_timeout": int(b.BetweenBytesTimeout),
"connect_timeout": int(b.ConnectTimeout),
"error_threshold": int(b.ErrorThreshold),
"first_byte_timeout": int(b.FirstByteTimeout),
"max_conn": int(b.MaxConn),
"port": int(b.Port),
"ssl_check_cert": b.SSLCheckCert,
"weight": int(b.Weight),
}
bl = append(bl, nb)
}
return bl
}
// findService finds a Fastly Service via the ListServices endpoint, returning
// the Service if found.
//
// Fastly API does not include any "deleted_at" type parameter to indicate
// that a Service has been deleted. GET requests to a deleted Service will
// return 200 OK and have the full output of the Service for an unknown time
// (days, in my testing). In order to determine if a Service is deleted, we
// need to hit /service and loop the returned Services, searching for the one
// in question. This endpoint only returns active or "alive" services. If the
// Service is not included, then it's "gone"
//
// Returns a fastlyNoServiceFoundErr error if the Service is not found in the
// ListServices response.
func findService(id string, meta interface{}) (*gofastly.Service, error) {
conn := meta.(*FastlyClient).conn
l, err := conn.ListServices(&gofastly.ListServicesInput{})
if err != nil {
return nil, fmt.Errorf("[WARN] Error listing servcies when deleting Fastly Service (%s): %s", id, err)
}
for _, s := range l {
if s.ID == id {
log.Printf("[DEBUG] Found Service (%s)", id)
return s, nil
}
}
return nil, fastlyNoServiceFoundErr
}

View File

@ -0,0 +1,409 @@
package fastly
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
gofastly "github.com/sethvargo/go-fastly"
)
func TestResourceFastlyFlattenDomains(t *testing.T) {
cases := []struct {
remote []*gofastly.Domain
local []map[string]interface{}
}{
{
remote: []*gofastly.Domain{
&gofastly.Domain{
Name: "test.notexample.com",
Comment: "not comment",
},
},
local: []map[string]interface{}{
map[string]interface{}{
"name": "test.notexample.com",
"comment": "not comment",
},
},
},
{
remote: []*gofastly.Domain{
&gofastly.Domain{
Name: "test.notexample.com",
},
},
local: []map[string]interface{}{
map[string]interface{}{
"name": "test.notexample.com",
"comment": "",
},
},
},
}
for _, c := range cases {
out := flattenDomains(c.remote)
if !reflect.DeepEqual(out, c.local) {
t.Fatalf("Error matching:\nexpected: %#v\ngot: %#v", c.local, out)
}
}
}
func TestResourceFastlyFlattenBackend(t *testing.T) {
cases := []struct {
remote []*gofastly.Backend
local []map[string]interface{}
}{
{
remote: []*gofastly.Backend{
&gofastly.Backend{
Name: "test.notexample.com",
Address: "www.notexample.com",
Port: uint(80),
AutoLoadbalance: true,
BetweenBytesTimeout: uint(10000),
ConnectTimeout: uint(1000),
ErrorThreshold: uint(0),
FirstByteTimeout: uint(15000),
MaxConn: uint(200),
SSLCheckCert: true,
Weight: uint(100),
},
},
local: []map[string]interface{}{
map[string]interface{}{
"name": "test.notexample.com",
"address": "www.notexample.com",
"port": 80,
"auto_loadbalance": true,
"between_bytes_timeout": 10000,
"connect_timeout": 1000,
"error_threshold": 0,
"first_byte_timeout": 15000,
"max_conn": 200,
"ssl_check_cert": true,
"weight": 100,
},
},
},
}
for _, c := range cases {
out := flattenBackends(c.remote)
if !reflect.DeepEqual(out, c.local) {
t.Fatalf("Error matching:\nexpected: %#v\ngot: %#v", c.local, out)
}
}
}
func TestAccFastlyServiceV1_updateDomain(t *testing.T) {
var service gofastly.ServiceDetail
name := fmt.Sprintf("tf-test-%s", acctest.RandString(10))
nameUpdate := fmt.Sprintf("tf-test-%s", acctest.RandString(10))
domainName1 := fmt.Sprintf("%s.notadomain.com", acctest.RandString(10))
domainName2 := fmt.Sprintf("%s.notadomain.com", acctest.RandString(10))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckServiceV1Destroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccServiceV1Config(name, domainName1),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceV1Exists("fastly_service_v1.foo", &service),
testAccCheckFastlyServiceV1Attributes(&service, name, []string{domainName1}),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "name", name),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "active_version", "1"),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "domain.#", "1"),
),
},
resource.TestStep{
Config: testAccServiceV1Config_domainUpdate(nameUpdate, domainName1, domainName2),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceV1Exists("fastly_service_v1.foo", &service),
testAccCheckFastlyServiceV1Attributes(&service, nameUpdate, []string{domainName1, domainName2}),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "name", nameUpdate),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "active_version", "2"),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "domain.#", "2"),
),
},
},
})
}
func TestAccFastlyServiceV1_updateBackend(t *testing.T) {
var service gofastly.ServiceDetail
name := fmt.Sprintf("tf-test-%s", acctest.RandString(10))
backendName := fmt.Sprintf("%s.aws.amazon.com", acctest.RandString(3))
backendName2 := fmt.Sprintf("%s.aws.amazon.com", acctest.RandString(3))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckServiceV1Destroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccServiceV1Config_backend(name, backendName),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceV1Exists("fastly_service_v1.foo", &service),
testAccCheckFastlyServiceV1Attributes_backends(&service, name, []string{backendName}),
),
},
resource.TestStep{
Config: testAccServiceV1Config_backend_update(name, backendName, backendName2),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceV1Exists("fastly_service_v1.foo", &service),
testAccCheckFastlyServiceV1Attributes_backends(&service, name, []string{backendName, backendName2}),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "active_version", "2"),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "backend.#", "2"),
),
},
},
})
}
func TestAccFastlyServiceV1_basic(t *testing.T) {
var service gofastly.ServiceDetail
name := fmt.Sprintf("tf-test-%s", acctest.RandString(10))
domainName := fmt.Sprintf("%s.notadomain.com", acctest.RandString(10))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckServiceV1Destroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccServiceV1Config(name, domainName),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceV1Exists("fastly_service_v1.foo", &service),
testAccCheckFastlyServiceV1Attributes(&service, name, []string{domainName}),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "name", name),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "active_version", "1"),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "domain.#", "1"),
),
},
},
})
}
func testAccCheckServiceV1Exists(n string, service *gofastly.ServiceDetail) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No Service ID is set")
}
conn := testAccProvider.Meta().(*FastlyClient).conn
latest, err := conn.GetServiceDetails(&gofastly.GetServiceInput{
ID: rs.Primary.ID,
})
if err != nil {
return err
}
*service = *latest
return nil
}
}
func testAccCheckFastlyServiceV1Attributes(service *gofastly.ServiceDetail, name string, domains []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if service.Name != name {
return fmt.Errorf("Bad name, expected (%s), got (%s)", name, service.Name)
}
conn := testAccProvider.Meta().(*FastlyClient).conn
domainList, err := conn.ListDomains(&gofastly.ListDomainsInput{
Service: service.ID,
Version: service.ActiveVersion.Number,
})
if err != nil {
return fmt.Errorf("[ERR] Error looking up Domains for (%s), version (%s): %s", service.Name, service.ActiveVersion.Number, err)
}
expected := len(domains)
for _, d := range domainList {
for _, e := range domains {
if d.Name == e {
expected--
}
}
}
if expected > 0 {
return fmt.Errorf("Domain count mismatch, expected: %#v, got: %#v", domains, domainList)
}
return nil
}
}
func testAccCheckFastlyServiceV1Attributes_backends(service *gofastly.ServiceDetail, name string, backends []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if service.Name != name {
return fmt.Errorf("Bad name, expected (%s), got (%s)", name, service.Name)
}
conn := testAccProvider.Meta().(*FastlyClient).conn
backendList, err := conn.ListBackends(&gofastly.ListBackendsInput{
Service: service.ID,
Version: service.ActiveVersion.Number,
})
if err != nil {
return fmt.Errorf("[ERR] Error looking up Backends for (%s), version (%s): %s", service.Name, service.ActiveVersion.Number, err)
}
expected := len(backendList)
for _, b := range backendList {
for _, e := range backends {
if b.Address == e {
expected--
}
}
}
if expected > 0 {
return fmt.Errorf("Backend count mismatch, expected: %#v, got: %#v", backends, backendList)
}
return nil
}
}
func testAccCheckServiceV1Destroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "fastly_service_v1" {
continue
}
conn := testAccProvider.Meta().(*FastlyClient).conn
l, err := conn.ListServices(&gofastly.ListServicesInput{})
if err != nil {
return fmt.Errorf("[WARN] Error listing servcies when deleting Fastly Service (%s): %s", rs.Primary.ID, err)
}
for _, s := range l {
if s.ID == rs.Primary.ID {
// service still found
return fmt.Errorf("[WARN] Tried deleting Service (%s), but was still found", rs.Primary.ID)
}
}
}
return nil
}
func testAccServiceV1Config(name, domain string) string {
return fmt.Sprintf(`
resource "fastly_service_v1" "foo" {
name = "%s"
domain {
name = "%s"
comment = "tf-testing-domain"
}
backend {
address = "aws.amazon.com"
name = "amazon docs"
}
force_destroy = true
}`, name, domain)
}
func testAccServiceV1Config_domainUpdate(name, domain1, domain2 string) string {
return fmt.Sprintf(`
resource "fastly_service_v1" "foo" {
name = "%s"
domain {
name = "%s"
comment = "tf-testing-domain"
}
domain {
name = "%s"
comment = "tf-testing-other-domain"
}
backend {
address = "aws.amazon.com"
name = "amazon docs"
}
force_destroy = true
}`, name, domain1, domain2)
}
func testAccServiceV1Config_backend(name, backend string) string {
return fmt.Sprintf(`
resource "fastly_service_v1" "foo" {
name = "%s"
domain {
name = "test.notadomain.com"
comment = "tf-testing-domain"
}
backend {
address = "%s"
name = "tf -test backend"
}
force_destroy = true
}`, name, backend)
}
func testAccServiceV1Config_backend_update(name, backend, backend2 string) string {
return fmt.Sprintf(`
resource "fastly_service_v1" "foo" {
name = "%s"
default_ttl = 3400
domain {
name = "test.notadomain.com"
comment = "tf-testing-domain"
}
backend {
address = "%s"
name = "tf-test-backend"
}
backend {
address = "%s"
name = "tf-test-backend-other"
}
force_destroy = true
}`, name, backend, backend2)
}

View File

@ -22,6 +22,7 @@ body.layout-dnsimple,
body.layout-docker,
body.layout-dyn,
body.layout-github,
body.layout-fastly,
body.layout-google,
body.layout-heroku,
body.layout-influxdb,

View File

@ -0,0 +1,80 @@
---
layout: "fastly"
page_title: "Provider: Fastly"
sidebar_current: "docs-fastly-index"
description: |-
Fastly
---
# Fastly Provider
The Fastly provider is used to interact with the content delivery network (CDN)
provided by Fastly.
In order to use this Provider, you must have an active account with Fastly.
Pricing and signup information can be found at https://www.fastly.com/signup
Use the navigation to the left to read about the available resources.
## Example Usage
```
# Configure the Fastly Provider
provider "fastly" {
api_key = "test"
}
# Create a Service
resource "fastly_service_v1" "myservice" {
name = "myawesometestservice"
...
}
```
## Authentication
The Fastly provider offers an API key based method of providing credentials for
authentication. The following methods are supported, in this order, and
explained below:
- Static API key
- Environment variables
### Static API Key ###
Static credentials can be provided by adding a `api_key` in-line in the
fastly provider block:
Usage:
```
provider "fastly" {
api_key = "test"
}
```
The API key for an account can be found on the Account page: https://app.fastly.com/#account
###Environment variables
You can provide your API key via `FASTLY_API_KEY` environment variable,
representing your Fastly API key.
```
provider "fastly" {}
```
Usage:
```
$ export FASTLY_API_KEY="afastlyapikey"
$ terraform plan
```
## Argument Reference
The following arguments are supported in the `provider` block:
* `api_key` - (Optional) This is the API key. It must be provided, but
it can also be sourced from the `FASTLY_API_KEY` environment variable

View File

@ -0,0 +1,136 @@
---
layout: "fastly"
page_title: "Fastly: aws_vpc"
sidebar_current: "docs-fastly-resource-service-v1"
description: |-
Provides an Fastly Service
---
# fastly\_service\_v1
Provides an Fastly Service, representing the configuration for a website, app,
api, or anything else to be served through Fastly. A Service encompasses Domains
and Backends.
The Service resource requires a domain name that is correctly setup to direct
traffic to the Fastly service. See Fastly's guide on [Adding CNAME Records][2]
on their documentation site for guidance.
## Example Usage
Basic usage:
```
resource "fastly_service_v1" "demo" {
name = "demofastly"
domain {
name = "demo.notexample.com"
comment = "demo"
}
backend {
address = "127.0.0.1"
name = "localhost"
port = 80
}
force_destroy = true
}
```
Basic usage with an Amazon S3 Website:
```
resource "fastly_service_v1" "demo" {
name = "demofastly"
domain {
name = "demo.notexample.com"
comment = "demo"
}
backend {
address = "demo.notexample.com.s3-website-us-west-2.amazonaws.com"
name = "AWS S3 hosting"
port = 80
}
default_host = "${aws_s3_bucket.website.name}.s3-website-us-west-2.amazonaws.com"
force_destroy = true
}
resource "aws_s3_bucket" "website" {
bucket = "demo.notexample.com"
acl = "public-read"
website {
index_document = "index.html"
error_document = "error.html"
}
}
```
**Note:** For an AWS S3 Bucket, the Backend address is
`<domain>.s3-website-<region>.amazonaws.com`. The `default_host` attribute
should be set to `<bucket_name>.s3-website-<region>.amazonaws.com`. See the
Fastly documentation on [Amazon S3][1]
## Argument Reference
The following arguments are supported:
* `name` - (Required) The unique name for the Service to create
* `domain` - (Required) A set of Domain names to serve as entry points for your
Service. Defined below.
* `backend` - (Required) A set of Backends to service requests from your Domains.
Defined below.
* `default_host` - (Optional) The default hostname
* `default_ttl` - (Optional) The default Time-to-live (TTL) for requests
* `force_destroy` - (Optional) Services that are active cannot be destroyed. In
order to destroy the Service, set `force_destroy` to `true`. Default `false`.
The `domain` block supports:
* `name` - (Required) The domain that this Service will respond to
* `comment` - (Optional) An optional comment about the Domain
The `backend` block supports:
* `name` - (Required, string) Name for this Backend. Must be unique to this Service
* `address` - (Required, string) An IPv4, hostname, or IPv6 address for the Backend
* `auto_loadbalance` - (Optional, boolean) Denote if this Backend should be
included in the pool of backends that requests are load balanced against.
Default `true`
* `between_bytes_timeout` - (Optional) How long to wait between bytes in milliseconds. Default `10000`
* `connect_timeout` - (Optional) How long to wait for a timeout in milliseconds.
Default `1000`
* `error_threshold` - (Optional) Number of errors to allow before the Backend is marked as down. Default `0`
* `first_byte_timeout` - (Optional) How long to wait for the first bytes in milliseconds. Default `15000`
* `max_conn` - (Optional) Maximum number of connections for this Backend.
Default `200`
* `port` - (Optional) The port number Backend responds on. Default `80`
* `ssl_check_cert` - (Optional) Be strict on checking SSL certs. Default `true`
* `weight` - (Optional) How long to wait for the first bytes in milliseconds.
Default `100`
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the Service
* `name` Name of this service
* `active_version` - The currently active version of your Fastly Service
* `domain`  Set of Domains. See above for details
* `backend`  Set of Backends. See above for details
* `default_host`  Default host specified
* `default_ttl` - Default TTL
* `force_destroy` - Force the destruction of the Service on delete
[1]: https://docs.fastly.com/guides/integrations/amazon-s3
[2]: https://docs.fastly.com/guides/basic-setup/adding-cname-records

View File

@ -193,6 +193,10 @@
<a href="/docs/providers/github/index.html">Github</a>
</li>
<li<%= sidebar_current("docs-providers-fastly") %>>
<a href="/docs/providers/fastly/index.html">Fastly</a>
</li>
<li<%= sidebar_current("docs-providers-google") %>>
<a href="/docs/providers/google/index.html">Google Cloud</a>
</li>

View File

@ -0,0 +1,28 @@
<% wrap_layout :inner do %>
<% content_for :sidebar do %>
<div class="docs-sidebar hidden-print affix-top" role="complementary">
<ul class="nav docs-sidenav">
<li<%= sidebar_current("docs-home") %>>
<a href="/docs/providers/index.html">&laquo; Documentation Home</a>
</li>
<li<%= sidebar_current("docs-fastly-index") %>>
<a href="/docs/providers/fastly/index.html">Fastly Provider</a>
</li>
<li<%= sidebar_current(/^docs-fastly-resource/) %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-fastly-resource-service-v1") %>>
<a href="/docs/providers/fastly/r/service_v1.html">service_v1</a>
</li>
</ul>
</li>
</ul>
</div>
<% end %>
<%= yield %>
<% end %>