Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

provider/fastly: add support for custom VCL configuration (supersedes #6587) #6662

Merged
merged 3 commits into from
May 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions builtin/providers/fastly/resource_fastly_service_v1.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package fastly

import (
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"log"
Expand Down Expand Up @@ -469,11 +471,48 @@ func resourceServiceV1() *schema.Resource {
},
},
},
"vcl": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "A name to refer to this VCL configuration",
},
"content": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "The contents of this VCL configuration",
StateFunc: func(v interface{}) string {
switch v.(type) {
case string:
hash := sha1.Sum([]byte(v.(string)))
return hex.EncodeToString(hash[:])
default:
return ""
}
},
},
"main": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Should this VCL configuation be the main configuration",
},
},
},
},
},
}
}

func resourceServiceV1Create(d *schema.ResourceData, meta interface{}) error {
if err := validateVCLs(d); err != nil {
return err
}

conn := meta.(*FastlyClient).conn
service, err := conn.CreateService(&gofastly.CreateServiceInput{
Name: d.Get("name").(string),
Expand All @@ -489,6 +528,10 @@ func resourceServiceV1Create(d *schema.ResourceData, meta interface{}) error {
}

func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
if err := validateVCLs(d); err != nil {
return err
}

conn := meta.(*FastlyClient).conn

// Update Name. No new verions is required for this
Expand Down Expand Up @@ -517,6 +560,7 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
"s3logging",
"condition",
"request_setting",
"vcl",
} {
if d.HasChange(v) {
needsChange = true
Expand Down Expand Up @@ -976,6 +1020,71 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error {
}
}
}
// Find differences in VCLs
if d.HasChange("vcl") {
// Note: as above with Gzip and S3 logging, we don't utilize the PUT
// endpoint to update a VCL, we simply destroy it and create a new one.
oldVCLVal, newVCLVal := d.GetChange("vcl")
if oldVCLVal == nil {
oldVCLVal = new(schema.Set)
}
if newVCLVal == nil {
newVCLVal = new(schema.Set)
}

oldVCLSet := oldVCLVal.(*schema.Set)
newVCLSet := newVCLVal.(*schema.Set)

remove := oldVCLSet.Difference(newVCLSet).List()
add := newVCLSet.Difference(oldVCLSet).List()

// Delete removed VCL configurations
for _, dRaw := range remove {
df := dRaw.(map[string]interface{})
opts := gofastly.DeleteVCLInput{
Service: d.Id(),
Version: latestVersion,
Name: df["name"].(string),
}

log.Printf("[DEBUG] Fastly VCL Removal opts: %#v", opts)
err := conn.DeleteVCL(&opts)
if err != nil {
return err
}
}
// POST new VCL configurations
for _, dRaw := range add {
df := dRaw.(map[string]interface{})
opts := gofastly.CreateVCLInput{
Service: d.Id(),
Version: latestVersion,
Name: df["name"].(string),
Content: df["content"].(string),
}

log.Printf("[DEBUG] Fastly VCL Addition opts: %#v", opts)
_, err := conn.CreateVCL(&opts)
if err != nil {
return err
}

// if this new VCL is the main
if df["main"].(bool) {
opts := gofastly.ActivateVCLInput{
Service: d.Id(),
Version: latestVersion,
Name: df["name"].(string),
}
log.Printf("[DEBUG] Fastly VCL activation opts: %#v", opts)
_, err := conn.ActivateVCL(&opts)
if err != nil {
return err
}

}
}
}

// validate version
log.Printf("[DEBUG] Validating Fastly Service (%s), Version (%s)", d.Id(), latestVersion)
Expand Down Expand Up @@ -1173,6 +1282,21 @@ func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error {
if err := d.Set("request_setting", rl); err != nil {
log.Printf("[WARN] Error setting Request Settings for (%s): %s", d.Id(), err)
}
// refresh VCLs
log.Printf("[DEBUG] Refreshing VCLs for (%s)", d.Id())
vclList, err := conn.ListVCLs(&gofastly.ListVCLsInput{
Service: d.Id(),
Version: s.ActiveVersion.Number,
})
if err != nil {
return fmt.Errorf("[ERR] Error looking up VCLs for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err)
}

vl := flattenVCLs(vclList)

if err := d.Set("vcl", vl); err != nil {
log.Printf("[WARN] Error setting VCLs for (%s): %s", d.Id(), err)
}

} else {
log.Printf("[DEBUG] Active Version for Service (%s) is empty, no state to refresh", d.Id())
Expand Down Expand Up @@ -1538,3 +1662,51 @@ func buildRequestSetting(requestSettingMap interface{}) (*gofastly.CreateRequest

return &opts, nil
}
func flattenVCLs(vclList []*gofastly.VCL) []map[string]interface{} {
var vl []map[string]interface{}
for _, vcl := range vclList {
// Convert VCLs to a map for saving to state.
vclMap := map[string]interface{}{
"name": vcl.Name,
"content": vcl.Content,
"main": vcl.Main,
}

// prune any empty values that come from the default string value in structs
for k, v := range vclMap {
if v == "" {
delete(vclMap, k)
}
}

vl = append(vl, vclMap)
}

return vl
}

func validateVCLs(d *schema.ResourceData) error {
// TODO: this would be nice to move into a resource/collection validation function, once that is available
// (see https://github.com/hashicorp/terraform/pull/4348 and https://github.com/hashicorp/terraform/pull/6508)
vcls, exists := d.GetOk("vcl")
if !exists {
return nil
}

numberOfMainVCLs, numberOfIncludeVCLs := 0, 0
for _, vclElem := range vcls.(*schema.Set).List() {
vcl := vclElem.(map[string]interface{})
if mainVal, hasMain := vcl["main"]; hasMain && mainVal.(bool) {
numberOfMainVCLs++
} else {
numberOfIncludeVCLs++
}
}
if numberOfMainVCLs == 0 && numberOfIncludeVCLs > 0 {
return fmt.Errorf("if you include VCL configurations, one of them should have main = true")
}
if numberOfMainVCLs > 1 {
return fmt.Errorf("you cannot have more than one VCL configuration with main = true")
}
return nil
}
152 changes: 152 additions & 0 deletions builtin/providers/fastly/resource_fastly_service_v1_vcl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package fastly

import (
"fmt"
"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_VCL_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))

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckServiceV1Destroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccServiceV1VCLConfig(name, domainName1),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceV1Exists("fastly_service_v1.foo", &service),
testAccCheckFastlyServiceV1VCLAttributes(&service, name, 1),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "name", name),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "vcl.#", "1"),
),
},

resource.TestStep{
Config: testAccServiceV1VCLConfig_update(name, domainName1),
Check: resource.ComposeTestCheckFunc(
testAccCheckServiceV1Exists("fastly_service_v1.foo", &service),
testAccCheckFastlyServiceV1VCLAttributes(&service, name, 2),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "name", name),
resource.TestCheckResourceAttr(
"fastly_service_v1.foo", "vcl.#", "2"),
),
},
},
})
}

func testAccCheckFastlyServiceV1VCLAttributes(service *gofastly.ServiceDetail, name string, vclCount int) 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
vclList, err := conn.ListVCLs(&gofastly.ListVCLsInput{
Service: service.ID,
Version: service.ActiveVersion.Number,
})

if err != nil {
return fmt.Errorf("[ERR] Error looking up VCL for (%s), version (%s): %s", service.Name, service.ActiveVersion.Number, err)
}

if len(vclList) != vclCount {
return fmt.Errorf("VCL count mismatch, expected (%d), got (%d)", vclCount, len(vclList))
}

return nil
}
}

func testAccServiceV1VCLConfig(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"
}
vcl {
name = "my_custom_main_vcl"
content = <<EOF
sub vcl_recv {
#FASTLY recv
if (req.request != "HEAD" && req.request != "GET" && req.request != "FASTLYPURGE") {
return(pass);
}
return(lookup);
}
EOF
main = true
}
force_destroy = true
}`, name, domain)
}

func testAccServiceV1VCLConfig_update(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"
}
vcl {
name = "my_custom_main_vcl"
content = <<EOF
sub vcl_recv {
#FASTLY recv
if (req.request != "HEAD" && req.request != "GET" && req.request != "FASTLYPURGE") {
return(pass);
}
return(lookup);
}
EOF
main = true
}
vcl {
name = "other_vcl"
content = <<EOF
sub vcl_error {
#FASTLY error
}
EOF
}
force_destroy = true
}`, name, domain)
}
Loading