provider/fastly: Add support for Conditions for Fastly Services (#6481)

* provider/fastly: Add support for Conditions for Fastly Services

Docs here:

- https://docs.fastly.com/guides/conditions/

Also Bump go-fastly version for domain support in S3 Logging
This commit is contained in:
Clint 2016-05-09 13:08:13 -05:00 committed by James Nugent
parent dbdf9f6c84
commit 3eee40cd98
4 changed files with 284 additions and 35 deletions

View File

@ -55,6 +55,39 @@ func resourceServiceV1() *schema.Resource {
},
},
"condition": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"statement": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "The statement used to determine if the condition is met",
StateFunc: func(v interface{}) string {
value := v.(string)
// Trim newlines and spaces, to match Fastly API
return strings.TrimSpace(value)
},
},
"priority": &schema.Schema{
Type: schema.TypeInt,
Required: true,
Description: "A number used to determine the order in which multiple conditions execute. Lower numbers execute first",
},
"type": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Type of the condition, either `REQUEST`, `RESPONSE`, or `CACHE`",
},
},
},
},
"default_ttl": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
@ -409,6 +442,7 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
"header",
"gzip",
"s3logging",
"condition",
} {
if d.HasChange(v) {
needsChange = true
@ -463,13 +497,70 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
}
}
// Conditions need to be updated first, as they can be referenced by other
// configuraiton objects (Backends, Request Headers, etc)
// Find difference in Conditions
if d.HasChange("condition") {
// Note: we don't utilize the PUT endpoint to update these objects, we simply
// destroy any that have changed, and create new ones with the updated
// values. This is how Terraform works with nested sub resources, we only
// get the full diff not a partial set item diff. Because this is done
// on a new version of the Fastly Service configuration, this is considered safe
oc, nc := d.GetChange("condition")
if oc == nil {
oc = new(schema.Set)
}
if nc == nil {
nc = new(schema.Set)
}
ocs := oc.(*schema.Set)
ncs := nc.(*schema.Set)
removeConditions := ocs.Difference(ncs).List()
addConditions := ncs.Difference(ocs).List()
// DELETE old Conditions
for _, cRaw := range removeConditions {
cf := cRaw.(map[string]interface{})
opts := gofastly.DeleteConditionInput{
Service: d.Id(),
Version: latestVersion,
Name: cf["name"].(string),
}
log.Printf("[DEBUG] Fastly Conditions Removal opts: %#v", opts)
err := conn.DeleteCondition(&opts)
if err != nil {
return err
}
}
// POST new Conditions
for _, cRaw := range addConditions {
cf := cRaw.(map[string]interface{})
opts := gofastly.CreateConditionInput{
Service: d.Id(),
Version: latestVersion,
Name: cf["name"].(string),
Type: cf["type"].(string),
// need to trim leading/tailing spaces, incase the config has HEREDOC
// formatting and contains a trailing new line
Statement: strings.TrimSpace(cf["statement"].(string)),
Priority: cf["priority"].(int),
}
log.Printf("[DEBUG] Create Conditions Opts: %#v", opts)
_, err := conn.CreateCondition(&opts)
if err != nil {
return err
}
}
}
// Find differences in domains
if d.HasChange("domain") {
// 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)
@ -523,12 +614,6 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
// 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)
@ -558,6 +643,7 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
}
}
// Find and post new Backends
for _, dRaw := range addBackends {
df := dRaw.(map[string]interface{})
opts := gofastly.CreateBackendInput{
@ -585,11 +671,6 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
}
if d.HasChange("header") {
// Note: we don't utilize the PUT endpoint to update a Header, we simply
// destroy it and create a new one. This is how Terraform works with nested
// sub resources, we only get the full diff not a partial set item diff.
// Because this is done on a new version of the configuration, this is
// considered safe
oh, nh := d.GetChange("header")
if oh == nil {
oh = new(schema.Set)
@ -640,11 +721,6 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
// Find differences in Gzips
if d.HasChange("gzip") {
// Note: we don't utilize the PUT endpoint to update a Gzip rule, we simply
// destroy it and create a new one. This is how Terraform works with nested
// sub resources, we only get the full diff not a partial set item diff.
// Because this is done on a new version of the configuration, this is
// considered safe
og, ng := d.GetChange("gzip")
if og == nil {
og = new(schema.Set)
@ -714,12 +790,6 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
// find difference in s3logging
if d.HasChange("s3logging") {
// POST new Logging
// Note: we don't utilize the PUT endpoint to update a S3 Logs, 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
os, ns := d.GetChange("s3logging")
if os == nil {
os = new(schema.Set)
@ -947,6 +1017,23 @@ func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error {
log.Printf("[WARN] Error setting S3 Logging for (%s): %s", d.Id(), err)
}
// refresh Conditions
log.Printf("[DEBUG] Refreshing Conditions for (%s)", d.Id())
conditionList, err := conn.ListConditions(&gofastly.ListConditionsInput{
Service: d.Id(),
Version: s.ActiveVersion.Number,
})
if err != nil {
return fmt.Errorf("[ERR] Error looking up Conditions for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
}
cl := flattenConditions(conditionList)
if err := d.Set("condition", cl); err != nil {
log.Printf("[WARN] Error setting Conditions for (%s): %s", d.Id(), err)
}
} else {
log.Printf("[DEBUG] Active Version for Service (%s) is empty, no state to refresh", d.Id())
}
@ -1215,3 +1302,27 @@ func flattenS3s(s3List []*gofastly.S3) []map[string]interface{} {
return sl
}
func flattenConditions(conditionList []*gofastly.Condition) []map[string]interface{} {
var cl []map[string]interface{}
for _, c := range conditionList {
// Convert Conditions to a map for saving to state.
nc := map[string]interface{}{
"name": c.Name,
"statement": c.Statement,
"type": c.Type,
"priority": c.Priority,
}
// prune any empty values that come from the default string value in structs
for k, v := range nc {
if v == "" {
delete(nc, k)
}
}
cl = append(cl, nc)
}
return cl
}

View File

@ -0,0 +1,122 @@
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 TestAccFastlyServiceV1_conditional_basic(t *testing.T) {
var service gofastly.ServiceDetail
name := fmt.Sprintf("tf-test-%s", acctest.RandString(10))
domainName1 := fmt.Sprintf("%s.notadomain.com", acctest.RandString(10))
con1 := gofastly.Condition{
Name: "some amz condition",
Priority: 10,
Type: "REQUEST",
Statement: `req.url ~ "^/yolo/"`,
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckServiceV1Destroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccServiceV1ConditionConfig(name, domainName1),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceV1Exists("fastly_service_v1.foo", &service),
testAccCheckFastlyServiceV1ConditionalAttributes(&service, name, []*gofastly.Condition{&con1}),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "name", name),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "condition.#", "1"),
),
},
},
})
}
func testAccCheckFastlyServiceV1ConditionalAttributes(service *gofastly.ServiceDetail, name string, conditions []*gofastly.Condition) 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
conditionList, err := conn.ListConditions(&gofastly.ListConditionsInput{
Service: service.ID,
Version: service.ActiveVersion.Number,
})
if err != nil {
return fmt.Errorf("[ERR] Error looking up Conditions for (%s), version (%s): %s", service.Name, service.ActiveVersion.Number, err)
}
if len(conditionList) != len(conditions) {
return fmt.Errorf("Error: mis match count of conditions, expected (%d), got (%d)", len(conditions), len(conditionList))
}
var found int
for _, c := range conditions {
for _, lc := range conditionList {
if c.Name == lc.Name {
// we don't know these things ahead of time, so populate them now
c.ServiceID = service.ID
c.Version = service.ActiveVersion.Number
if !reflect.DeepEqual(c, lc) {
return fmt.Errorf("Bad match Conditions match, expected (%#v), got (%#v)", c, lc)
}
found++
}
}
}
if found != len(conditions) {
return fmt.Errorf("Error matching Conditions rules")
}
return nil
}
}
func testAccServiceV1ConditionConfig(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"
}
header {
destination = "http.x-amz-request-id"
type = "cache"
action = "delete"
name = "remove x-amz-request-id"
}
condition {
name = "some amz condition"
type = "REQUEST"
statement = "req.url ~ \"^/yolo/\""
priority = 10
}
force_destroy = true
}`, name, domain)
}

View File

@ -7,14 +7,14 @@ import (
// Version represents a distinct configuration version.
type Version struct {
Number string `mapstructure:"number"`
Comment string `mapstructure:"comment"`
ServiceID string `mapstructure:"service_id"`
Active bool `mapstructure:"active"`
Locked bool `mapstructure:"locked"`
Deployed bool `mapstructure:"deployed"`
Staging bool `mapstructure:"staging"`
Testing bool `mapstructure:"testing"`
Number string `mapstructure:"number"`
Comment string `mapstructure:"comment"`
ServiceID string `mapstructure:"service_id"`
Active bool `mapstructure:"active"`
Locked bool `mapstructure:"locked"`
Deployed bool `mapstructure:"deployed"`
Staging bool `mapstructure:"staging"`
Testing bool `mapstructure:"testing"`
}
// versionsByNumber is a sortable list of versions. This is used by the version

View File

@ -100,6 +100,8 @@ The following arguments are supported:
Service. Defined below
* `backend` - (Required) A set of Backends to service requests from your Domains.
Defined below
* `condition` - (Optional) A set of conditions to add logic to any basic
configuration object in this service. Defined below
* `gzip` - (Required) A set of gzip rules to control automatic gzipping of
content. Defined below
* `header` - (Optional) A set of Headers to manipulate for each request. Defined
@ -135,6 +137,20 @@ Default `200`
* `ssl_check_cert` - (Optional) Be strict on checking SSL certs. Default `true`
* `weight` - (Optional) The [portion of traffic](https://docs.fastly.com/guides/performance-tuning/load-balancing-configuration.html#how-weight-affects-load-balancing) to send to this Backend. Each Backend receives `weight / total` of the traffic. Default `100`
The `condition` block supports allows you to add logic to any basic configuration
object in a service. See Fastly's documentation
["About Conditions"](https://docs.fastly.com/guides/conditions/about-conditions)
for more detailed information on using Conditions. The Condition `name` can be
used in the `request_condition`, `response_condition`, or
`cache_condition` attributes of other block settings
* `name` - (Required) A unique name of the condition
* `statement` - (Required) The statement used to determine if the condition is met
* `priority` - (Required) A number used to determine the order in which multiple
conditions execute. Lower numbers execute first
* `type` - (Required) Type of the condition, either `REQUEST` (req), `RESPONSE`
(req, resp), or `CACHE` (req, beresp)
The `gzip` block supports:
* `name` - (Required) A unique name