From dc6e02e56d36f58b7cc54a9cc3cccac91f6f0d09 Mon Sep 17 00:00:00 2001 From: clint shryock Date: Wed, 1 Jun 2016 14:44:40 -0500 Subject: [PATCH] provider/cloudflare: Add migration for v1 to v4 API libraries - mock out api calls --- .../cloudflare/resource_cloudflare_record.go | 2 + .../resource_cloudflare_record_migrate.go | 95 ++++++ ...resource_cloudflare_record_migrate_test.go | 291 ++++++++++++++++++ 3 files changed, 388 insertions(+) create mode 100644 builtin/providers/cloudflare/resource_cloudflare_record_migrate.go create mode 100644 builtin/providers/cloudflare/resource_cloudflare_record_migrate_test.go diff --git a/builtin/providers/cloudflare/resource_cloudflare_record.go b/builtin/providers/cloudflare/resource_cloudflare_record.go index a738aa7262a2..58b680946c38 100644 --- a/builtin/providers/cloudflare/resource_cloudflare_record.go +++ b/builtin/providers/cloudflare/resource_cloudflare_record.go @@ -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, diff --git a/builtin/providers/cloudflare/resource_cloudflare_record_migrate.go b/builtin/providers/cloudflare/resource_cloudflare_record_migrate.go new file mode 100644 index 000000000000..d71d3fa280ec --- /dev/null +++ b/builtin/providers/cloudflare/resource_cloudflare_record_migrate.go @@ -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") +} diff --git a/builtin/providers/cloudflare/resource_cloudflare_record_migrate_test.go b/builtin/providers/cloudflare/resource_cloudflare_record_migrate_test.go new file mode 100644 index 000000000000..f2c6de732c51 --- /dev/null +++ b/builtin/providers/cloudflare/resource_cloudflare_record_migrate_test.go @@ -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": [] +}`