Skip to content

Commit

Permalink
provider/cloudflare: Add migration for v1 to v4 API libraries
Browse files Browse the repository at this point in the history
- mock out api calls
  • Loading branch information
catsby committed Jun 7, 2016
1 parent 4c7a31d commit dc6e02e
Show file tree
Hide file tree
Showing 3 changed files with 388 additions and 0 deletions.
2 changes: 2 additions & 0 deletions builtin/providers/cloudflare/resource_cloudflare_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ func resourceCloudFlareRecord() *schema.Resource {
Update: resourceCloudFlareRecordUpdate,
Delete: resourceCloudFlareRecordDelete,

SchemaVersion: 1,
MigrateState: resourceCloudFlareRecordMigrateState,
Schema: map[string]*schema.Schema{
"domain": &schema.Schema{
Type: schema.TypeString,
Expand Down
95 changes: 95 additions & 0 deletions builtin/providers/cloudflare/resource_cloudflare_record_migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package cloudflare

import (
"fmt"
"log"
"strconv"

"github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform/terraform"
)

func resourceCloudFlareRecordMigrateState(
v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
switch v {
case 0:
log.Println("[INFO] Found CloudFlare Record State v0; migrating to v1")
return migrateCloudFlareRecordStateV0toV1(is, meta)
default:
return is, fmt.Errorf("Unexpected schema version: %d", v)
}
}

func migrateCloudFlareRecordStateV0toV1(is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
if is.Empty() {
log.Println("[DEBUG] Empty InstanceState; nothing to migrate.")
return is, nil
}

log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes)
client := meta.(*cloudflare.API)

// look up new id based on attributes
domain := is.Attributes["domain"]
zoneId, err := client.ZoneIDByName(domain)
if err != nil {
return is, fmt.Errorf("Error finding zone %q: %s", domain, err)
}

// all other information is ignored in the DNSRecords call
searchRecord := cloudflare.DNSRecord{
Type: is.Attributes["type"],
Name: is.Attributes["hostname"],
Content: is.Attributes["value"],
}

records, err := client.DNSRecords(zoneId, searchRecord)
if err != nil {
return is, err
}

for _, r := range records {
if is.Attributes["ttl"] != "" {
v, err := strconv.Atoi(is.Attributes["ttl"])
if err != nil {
return is, fmt.Errorf("Error converting ttl to int in CloudFlare Record Migration")
}

if v != r.TTL {
continue
}
}

if is.Attributes["proxied"] != "" {
b, err := strconv.ParseBool(is.Attributes["proxied"])
if err != nil {
return is, fmt.Errorf("Error converting proxied to bool in CloudFlare Record Migration")
}

if b != r.Proxied {
continue
}
}

if is.Attributes["priority"] != "" {
v, err := strconv.Atoi(is.Attributes["priority"])
if err != nil {
return is, fmt.Errorf("Error converting priority to int in CloudFlare Record Migration")
}

if v != r.Priority {
continue
}
}

// assume record found
is.Attributes["id"] = r.ID
is.ID = r.ID
log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes)
return is, nil
}

// assume no record found
log.Printf("[DEBUG] Attributes after no migration: %#v", is.Attributes)
return is, fmt.Errorf("No matching Record found")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
package cloudflare

import (
"fmt"
"log"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform/terraform"
)

func TestCloudFlareRecordMigrateState(t *testing.T) {
// create the test server for mocking the API calls
ts := mockCloudFlareEnv()
defer ts.Close()

// Create a CloudFlare client, overriding the BaseURL
cfMeta, err := cloudflare.New(
"sometoken",
"someemail",
mockHTTPClient(ts.URL),
)

if err != nil {
t.Fatalf("Error building CloudFlare API: %s", err)
}

cases := map[string]struct {
StateVersion int
ID string
Attributes map[string]string
Expected string
ShouldFail bool
}{
"ttl_120": {
StateVersion: 0,
ID: "123456",
Attributes: map[string]string{
"id": "123456",
"name": "notthesub",
"hostname": "notthesub.hashicorptest.com",
"type": "A",
"content": "10.0.2.5",
"ttl": "120",
"zone_id": "1234567890",
"domain": "hashicorptest.com",
},
Expected: "7778f8766e583af8de0abfcd76c5dAAA",
},
"ttl_121": {
StateVersion: 0,
ID: "123456",
Attributes: map[string]string{
"id": "123456",
"name": "notthesub",
"hostname": "notthesub.hashicorptest.com",
"type": "A",
"content": "10.0.2.5",
"ttl": "121",
"zone_id": "1234567890",
"domain": "hashicorptest.com",
},
Expected: "5558f8766e583af8de0abfcd76c5dBBB",
},
"mx_priority": {
StateVersion: 0,
ID: "123456",
Attributes: map[string]string{
"id": "123456",
"name": "hashicorptest.com",
"type": "MX",
"content": "some.registrar-servers.com",
"ttl": "1",
"priority": "20",
"zone_id": "1234567890",
"domain": "hashicorptest.com",
},
Expected: "12342092cbc4c391be33ce548713bba3",
},
"mx_priority_mismatch": {
StateVersion: 0,
ID: "123456",
Attributes: map[string]string{
"id": "123456",
"type": "MX",
"name": "hashicorptest.com",
"content": "some.registrar-servers.com",
"ttl": "1",
"priority": "10",
"zone_id": "1234567890",
"domain": "hashicorptest.com",
},
Expected: "12342092cbc4c391be33ce548713bba3",
ShouldFail: true,
},
}

for tn, tc := range cases {
is := &terraform.InstanceState{
ID: tc.ID,
Attributes: tc.Attributes,
}
is, err := resourceCloudFlareRecordMigrateState(
tc.StateVersion, is, cfMeta)

if err != nil {
if tc.ShouldFail {
// expected error
continue
}
t.Fatalf("bad: %s, err: %#v", tn, err)
}

if is.ID != tc.Expected {
t.Fatalf("bad sg rule id: %s\n\n expected: %s", is.ID, tc.Expected)
}
}
}

// cloudflareEnv establishes a httptest server to mock out the CloudFlare API
// endpoints that we'll be calling.
func mockCloudFlareEnv() *httptest.Server {
endpoints := mockEndpoints()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
log.Printf("[DEBUG] Mocker server received request to %q", r.RequestURI)
rBase, err := url.ParseRequestURI(r.RequestURI)
if err != nil {
log.Fatalf("Failed to find the base path: %s")
}
for _, e := range endpoints {
if rBase.Path == e.BasePath {
fmt.Fprintln(w, e.Body)
w.WriteHeader(200)
return
}
}
w.WriteHeader(400)
}))

return ts
}

// Stub out the two CloudFlare API routes that will be called
func mockEndpoints() []*endpoint {
return []*endpoint{
&endpoint{
BasePath: "/zones",
Body: zoneResponse,
},
&endpoint{
BasePath: "/zones/1234567890/dns_records",
Body: dnsResponse,
},
}
}

type routes struct {
Endpoints []*endpoint
}
type endpoint struct {
BasePath string
Body string
}

// HTTPClient accepts a custom *http.Client for making API calls.
// This function is used as a callback of sorts to override any of the client
// options that you can't directly set on the struct
func mockHTTPClient(testURL string) cloudflare.Option {
return func(api *cloudflare.API) error {
api.BaseURL = testURL
return nil
}
}

const zoneResponse = `
{
"result": [
{
"id": "1234567890",
"name": "hashicorptest.com",
"status": "active",
"paused": false,
"type": "full",
"development_mode": 0
}
],
"result_info": {
"page": 1,
"per_page": 20,
"total_pages": 1,
"count": 1,
"total_count": 1
},
"success": true,
"errors": [],
"messages": []
}
`

const dnsResponse = `
{
"result": [
{
"id": "7778f8766e583af8de0abfcd76c5dAAA",
"type": "A",
"name": "notthesub.hashicorptest.com",
"content": "10.0.2.5",
"proxiable": false,
"proxied": false,
"ttl": 120,
"locked": false,
"zone_id": "1234567890",
"zone_name": "hashicorptest.com"
},
{
"id": "5558f8766e583af8de0abfcd76c5dBBB",
"type": "A",
"name": "notthesub.hashicorptest.com",
"content": "10.0.2.5",
"proxiable": false,
"proxied": false,
"ttl": 121,
"locked": false,
"zone_id": "1234567890",
"zone_name": "hashicorptest.com"
},
{
"id": "2220a9593ab869199b65c89bddf72ddd",
"type": "A",
"name": "maybethesub.hashicorptest.com",
"content": "10.0.3.5",
"proxiable": false,
"proxied": false,
"ttl": 120,
"locked": false,
"zone_id": "1234567890",
"zone_name": "hashicorptest.com"
},
{
"id": "888ffe3f93a31231ad6b0c6d09185eee",
"type": "A",
"name": "tftestingsubv616.hashicorptest.com",
"content": "52.39.212.111",
"proxiable": true,
"proxied": true,
"ttl": 1,
"locked": false,
"zone_id": "1234567890",
"zone_name": "hashicorptest.com"
},
{
"id": "98y6t9ba87e6ee3e6aeba8f3dc52c81b",
"type": "CNAME",
"name": "somecname.hashicorptest.com",
"content": "some.us-west-2.elb.amazonaws.com",
"proxiable": true,
"proxied": false,
"ttl": 120,
"locked": false,
"zone_id": "1234567890",
"zone_name": "hashicorptest.com"
},
{
"id": "12342092cbc4c391be33ce548713bba3",
"type": "MX",
"name": "hashicorptest.com",
"content": "some.registrar-servers.com",
"proxiable": false,
"proxied": false,
"ttl": 1,
"priority": 20,
"locked": false,
"zone_id": "1234567890",
"zone_name": "hashicorptest.com"
}
],
"result_info": {
"page": 1,
"per_page": 20,
"total_pages": 2,
"count": 20,
"total_count": 4
},
"success": true,
"errors": [],
"messages": []
}`

0 comments on commit dc6e02e

Please sign in to comment.