From 414d394354230335a9a02669e1bdd1e29d976bab Mon Sep 17 00:00:00 2001 From: Julian Vassev Date: Sun, 4 Feb 2018 15:16:15 -0800 Subject: [PATCH] Add Dyn Provider * add "dyn" provider * add several --dyn-* args to configure Dyn login * add github.com/nesv/go-dynect/dynect@0.6.0 to Gopkg and vender/ (the client of choice by Terraform) * make externdns.Version public so it can be stored when committing zone changes * add tutorial for Ingress resources and update root README.md file Dyn REST API is documented here: https://help.dyn.com/dns-api-knowledge-base/ Example usage: external-dns \ --provider=dyn \ --dyn-customer-name=acme \ --dyn-username=acme-api \ --dyn-password=t0pS3cr3t \ --domain-filter=portal.acme.com \ --zone-id-filter=acme.com \ --namespace=my-test-ns \ --log-level=debug \ --txt-prefix=_ --- Gopkg.lock | 8 +- Gopkg.toml | 4 + Makefile | 2 +- README.md | 2 + docs/tutorials/dyn.md | 145 +++++ main.go | 12 + pkg/apis/externaldns/types.go | 13 +- provider/dyn.go | 564 ++++++++++++++++++ provider/dyn_test.go | 298 +++++++++ vendor/github.com/nesv/go-dynect/CHANGELOG.md | 102 ++++ vendor/github.com/nesv/go-dynect/LICENSE.md | 21 + vendor/github.com/nesv/go-dynect/README.md | 43 ++ .../nesv/go-dynect/dynect/client.go | 281 +++++++++ .../nesv/go-dynect/dynect/client_test.go | 123 ++++ .../go-dynect/dynect/convenient_client.go | 242 ++++++++ .../dynect/convenient_client_test.go | 241 ++++++++ .../github.com/nesv/go-dynect/dynect/dsfs.go | 117 ++++ .../dynect/fixtures/convenient_create_mx.yaml | 161 +++++ .../fixtures/convenient_create_zone.yaml | 134 +++++ .../fixtures/convenient_delete_sub_zone.yaml | 153 +++++ .../fixtures/convenient_delete_zone.yaml | 101 ++++ .../dynect/fixtures/convenient_get_a.yaml | 104 ++++ .../fixtures/convenient_get_a_not_found.yaml | 79 +++ .../dynect/fixtures/convenient_get_cname.yaml | 104 ++++ .../fixtures/fetching_all_zone_records.yaml | 269 +++++++++ .../dynect/fixtures/login_logout.yaml | 53 ++ .../dynect/fixtures/zones_request.yaml | 79 +++ .../nesv/go-dynect/dynect/helpers.go | 30 + .../github.com/nesv/go-dynect/dynect/job.go | 8 + .../github.com/nesv/go-dynect/dynect/json.go | 65 ++ .../nesv/go-dynect/dynect/record.go | 12 + .../nesv/go-dynect/dynect/records.go | 171 ++++++ .../github.com/nesv/go-dynect/dynect/zone.go | 9 + .../github.com/nesv/go-dynect/dynect/zones.go | 24 + 34 files changed, 3769 insertions(+), 5 deletions(-) create mode 100644 docs/tutorials/dyn.md create mode 100644 provider/dyn.go create mode 100644 provider/dyn_test.go create mode 100644 vendor/github.com/nesv/go-dynect/CHANGELOG.md create mode 100644 vendor/github.com/nesv/go-dynect/LICENSE.md create mode 100644 vendor/github.com/nesv/go-dynect/README.md create mode 100644 vendor/github.com/nesv/go-dynect/dynect/client.go create mode 100644 vendor/github.com/nesv/go-dynect/dynect/client_test.go create mode 100644 vendor/github.com/nesv/go-dynect/dynect/convenient_client.go create mode 100644 vendor/github.com/nesv/go-dynect/dynect/convenient_client_test.go create mode 100644 vendor/github.com/nesv/go-dynect/dynect/dsfs.go create mode 100644 vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_create_mx.yaml create mode 100644 vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_create_zone.yaml create mode 100644 vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_delete_sub_zone.yaml create mode 100644 vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_delete_zone.yaml create mode 100644 vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_get_a.yaml create mode 100644 vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_get_a_not_found.yaml create mode 100644 vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_get_cname.yaml create mode 100644 vendor/github.com/nesv/go-dynect/dynect/fixtures/fetching_all_zone_records.yaml create mode 100644 vendor/github.com/nesv/go-dynect/dynect/fixtures/login_logout.yaml create mode 100644 vendor/github.com/nesv/go-dynect/dynect/fixtures/zones_request.yaml create mode 100644 vendor/github.com/nesv/go-dynect/dynect/helpers.go create mode 100644 vendor/github.com/nesv/go-dynect/dynect/job.go create mode 100644 vendor/github.com/nesv/go-dynect/dynect/json.go create mode 100644 vendor/github.com/nesv/go-dynect/dynect/record.go create mode 100644 vendor/github.com/nesv/go-dynect/dynect/records.go create mode 100644 vendor/github.com/nesv/go-dynect/dynect/zone.go create mode 100644 vendor/github.com/nesv/go-dynect/dynect/zones.go diff --git a/Gopkg.lock b/Gopkg.lock index 408c7f950f..3c8c1d7b27 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -284,6 +284,12 @@ packages = ["pbutil"] revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" +[[projects]] + name = "github.com/nesv/go-dynect" + packages = ["dynect"] + revision = "cdd946344b54bdf7dbeac406c2f1fe93150f08ea" + version = "v0.6.0" + [[projects]] name = "github.com/pkg/errors" packages = ["."] @@ -616,6 +622,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "9056760f59de670713a6534dc50e7b4ef8f14b1ad25e5a4eb0e269d2d60c4a51" + inputs-digest = "7af57b8d195abce34060c82b92243ddba947f5d882be8f9a08b1aa827a9061fa" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index ba07244cbc..eef8ea8bd1 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -59,3 +59,7 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"] [[override]] name = "github.com/kubernetes/repo-infra" revision = "2d2eb5e12b4663fc4d764b5db9daab39334d3f37" + +[[constraint]] + name = "github.com/nesv/go-dynect" + version = "0.6.0" diff --git a/Makefile b/Makefile index 0a3ebaa3fd..e893623cea 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ SOURCES = $(shell find . -name '*.go') IMAGE ?= registry.opensource.zalan.do/teapot/$(BINARY) VERSION ?= $(shell git describe --tags --always --dirty) BUILD_FLAGS ?= -v -LDFLAGS ?= -X github.com/kubernetes-incubator/external-dns/pkg/apis/externaldns.version=$(VERSION) -w -s +LDFLAGS ?= -X github.com/kubernetes-incubator/external-dns/pkg/apis/externaldns.Version=$(VERSION) -w -s build: build/$(BINARY) diff --git a/README.md b/README.md index 29862893a4..87c1ea612a 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ ExternalDNS' current release is `v0.4`. This version allows you to keep selected * [DigitalOcean](https://www.digitalocean.com/products/networking) * [DNSimple](https://dnsimple.com/) * [Infoblox](https://www.infoblox.com/products/dns/) +* [Dyn](https://dyn.com/dns/) From this release, ExternalDNS can become aware of the records it is managing (enabled via `--registry=txt`), therefore ExternalDNS can safely manage non-empty hosted zones. We strongly encourage you to use `v0.4` with `--registry=txt` enabled and `--txt-owner-id` set to a unique value that doesn't change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (`--dry-run` flag) to see the changes to be submitted to your DNS Provider API. @@ -47,6 +48,7 @@ The following tutorials are provided: * [Cloudflare](docs/tutorials/cloudflare.md) * [DigitalOcean](docs/tutorials/digitalocean.md) * [Infoblox](docs/tutorials/infoblox.md) +* [Dyn](docs/tutorials/dyn.md) * Google Container Engine * [Using Google's Default Ingress Controller](docs/tutorials/gke.md) * [Using the Nginx Ingress Controller](docs/tutorials/nginx-ingress.md) diff --git a/docs/tutorials/dyn.md b/docs/tutorials/dyn.md new file mode 100644 index 0000000000..1bc8898f53 --- /dev/null +++ b/docs/tutorials/dyn.md @@ -0,0 +1,145 @@ +# Setting up ExternalDNS for Dyn + +## Creating a Dyn Configuration Secret + +For ExternalDNS to access the Dyn API, create a Kubernetes secret. + +To create the secret: + +``` +$ kubectl create secret generic external-dns \ + --from-literal=EXTERNAL_DNS_DYN_CUSTOMER_NAME=${DYN_CUSTOMER_NAME} \ + --from-literal=EXTERNAL_DNS_DYN_USERNAME=${DYN_USERNAME} \ + --from-literal=EXTERNAL_DNS_DYN_PASSWORD=${DYN_PASSWORD} +``` + +The credentials are the same ones created during account registration. As best practise, you are advised to +create an API-only user that is entitled to only the zones intended to be changed by ExternalDNS + +## Deploy ExternalDNS +The rest of this tutorial assumes you own `example.com` domain and your DNS provider is Dyn. Change `example.com` +with a domain/zone that you really own. + +In case of the dyn provider, the flag `--zone-id-filter` is mandatory as it specifies which zones to scan for records. Without it + + +Create a deployment file called `externaldns.yaml` with the following contents: + +``` +$ cat > externaldns.yaml < `EXTERNAL_DNS_FLAG=1` or `--flag value` -> `EXTERNAL_DNS_FLAG=value`") - app.Version(version) + app.Version(Version) app.DefaultEnvars() // Flags related to Kubernetes @@ -133,7 +137,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal) // Flags related to providers - app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, dnsimple, infoblox, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "inmemory") + app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "inmemory") app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter) app.Flag("google-project", "When using the Google provider, specify the Google project (required when --provider=google)").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject) @@ -147,6 +151,9 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("infoblox-wapi-password", "When using the Infoblox provider, specify the WAPI password (required when --provider=infoblox)").Default(defaultConfig.InfobloxWapiPassword).StringVar(&cfg.InfobloxWapiPassword) app.Flag("infoblox-wapi-version", "When using the Infoblox provider, specify the WAPI version (default: 2.3.1)").Default(defaultConfig.InfobloxWapiVersion).StringVar(&cfg.InfobloxWapiVersion) app.Flag("infoblox-ssl-verify", "When using the Infoblox provider, specify whether to verify the SSL certificate (default: true, disable with --no-infoblox-ssl-verify)").Default(strconv.FormatBool(defaultConfig.InfobloxSSLVerify)).BoolVar(&cfg.InfobloxSSLVerify) + app.Flag("dyn-customer-name", "When using the Dyn provider, specify the Customer Name").Default("").StringVar(&cfg.DynCustomerName) + app.Flag("dyn-username", "When using the Dyn provider, specify the Username").Default("").StringVar(&cfg.DynUsername) + app.Flag("dyn-password", "When using the Dyn provider, specify the pasword").Default("").StringVar(&cfg.DynPassword) app.Flag("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.InMemoryZones) // Flags related to policies diff --git a/provider/dyn.go b/provider/dyn.go new file mode 100644 index 0000000000..725e4b872b --- /dev/null +++ b/provider/dyn.go @@ -0,0 +1,564 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "fmt" + "os" + "strings" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/nesv/go-dynect/dynect" + + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/kubernetes-incubator/external-dns/plan" +) + +const ( + // 10 minutes default timeout if not configured using flags + dynDefaultTTL = 600 + // can store 20000 entries globally, that's about 4MB of memory + // may be made configurable in the future but 20K records seems like enough for a few zones + cacheMaxSize = 20000 + + // this prefix must be stripped from resource links before feeding them to dynect.Client.Do() + restAPIPrefix = "/REST/" +) + +// A simple non-thread-safe cache with TTL. The TTL of the records is used here to +// This cache is used to save on requests to DynAPI +type cache struct { + contents map[string]*entry +} + +type entry struct { + expires int64 + ep *endpoint.Endpoint +} + +func (c *cache) Put(link string, ep *endpoint.Endpoint) { + // flush the whole cache on overflow + if len(c.contents) >= cacheMaxSize { + c.contents = make(map[string]*entry) + } + + c.contents[link] = &entry{ + ep: ep, + expires: int64(time.Now().Unix()) + int64(ep.RecordTTL), + } +} + +func (c *cache) Get(link string) *endpoint.Endpoint { + result, ok := c.contents[link] + if !ok { + return nil + } + + now := int64(time.Now().Unix()) + + if result.expires < now { + delete(c.contents, link) + return nil + } + + return result.ep +} + +// DynConfig hold connection parameters to dyn.com and interanl state +type DynConfig struct { + DomainFilter DomainFilter + ZoneIDFilter ZoneIDFilter + DryRun bool + CustomerName string + Username string + Password string + AppVersion string + DynVersion string +} + +// DynProvider is the actual interface impl. +type dynProviderState struct { + DynConfig + Cache *cache +} + +// ZoneChange is missing from dynect: https://help.dyn.com/get-zone-changeset-api/ +type ZoneChange struct { + ID int `json:"id"` + UserID int `json:"user_id"` + Zone string `json:"zone"` + FQDN string `json:"FQDN"` + Serial int `json:"serial"` + TTL int `json:"ttl"` + Type string `json:"rdata_type"` + RData dynect.DataBlock `json:"rdata"` +} + +// ZoneChangesResponse is missing from dynect: https://help.dyn.com/get-zone-changeset-api/ +type ZoneChangesResponse struct { + dynect.ResponseBlock + Data []ZoneChange `json:"data"` +} + +// ZonePublishRequest is missing from dynect but the notes field is a nice place to let +// external-dns report some internal info during commit +type ZonePublishRequest struct { + Publish bool `json:"publish"` + Notes string `json:"notes"` +} + +// ZonePublisResponse holds the status after publish +type ZonePublisResponse struct { + dynect.ResponseBlock + Data map[string]interface{} `json:"data"` +} + +// NewDynProvider initializes a new Dyn Provider. +func NewDynProvider(config DynConfig) (Provider, error) { + return &dynProviderState{ + DynConfig: config, + Cache: &cache{ + contents: make(map[string]*entry), + }, + }, nil +} + +// filterAndFixLinks removes from `links` all the records we don't care about +// and strops the /REST/ prefix +func filterAndFixLinks(links []string, filter DomainFilter) []string { + var result []string + for _, link := range links { + + link = strings.TrimPrefix(link, restAPIPrefix) + + // covert resource link to just the FQDN so we can filter on it + domain := link[0:strings.LastIndexByte(link, '/')] + domain = domain[strings.LastIndexByte(domain, '/')+1:] + + // simply ignore all record types we don't care about + if !strings.HasPrefix(link, endpoint.RecordTypeA) && + !strings.HasPrefix(link, endpoint.RecordTypeCNAME) && + !strings.HasPrefix(link, endpoint.RecordTypeTXT) { + continue + } + + if filter.Match(domain) { + result = append(result, link) + } + } + + return result +} + +func fixMissingTTL(ttl endpoint.TTL) string { + i := dynDefaultTTL + if ttl.IsConfigured() { + i = int(ttl) + } + + return fmt.Sprintf("%d", i) +} + +// merge produces a singe list of records that can be used as a replacement. +// Dyn allows to replace all records with a single call +// Invariant: the result contains only elements from the updateNew parameter +func merge(updateOld, updateNew []*endpoint.Endpoint) []*endpoint.Endpoint { + findMatch := func(template *endpoint.Endpoint) *endpoint.Endpoint { + for _, new := range updateNew { + if template.DNSName == new.DNSName && + template.RecordType == new.RecordType { + return new + } + } + return nil + } + + var result []*endpoint.Endpoint + for _, old := range updateOld { + matchingNew := findMatch(old) + if matchingNew == nil { + // no match, shouldn't happen + continue + } + + if matchingNew.Target != old.Target { + // new target: always update, TTL will be overwritten too if necessary + result = append(result, matchingNew) + continue + } + + if matchingNew.RecordTTL != 0 && matchingNew.RecordTTL != old.RecordTTL { + // same target, but new non-zero TTL set in k8s, must update + // probably would happen only if there is a bug in the code calling the provider + result = append(result, matchingNew) + } + } + + return result +} + +// extractTarget populates the correct field given a record type. +// See dynect.DataBlock comments for details. Empty response means nothing +// was populated - basically an error +func extractTarget(recType string, data *dynect.DataBlock) string { + result := "" + if recType == endpoint.RecordTypeA { + result = data.Address + } + + if recType == endpoint.RecordTypeCNAME { + result = data.CName + result = strings.TrimSuffix(result, ".") + } + + if recType == endpoint.RecordTypeTXT { + result = data.TxtData + } + + return result +} + +// recordLinkToEndpoint makes an Endpoint given a resource link optinally making a remote call if a cached entry is expired +func (d *dynProviderState) recordLinkToEndpoint(client *dynect.Client, recordLink string) (*endpoint.Endpoint, error) { + result := d.Cache.Get(recordLink) + if result != nil { + log.Infof("Using cached endpoint for %s: %+v", recordLink, result) + return result, nil + } + + rec := dynect.RecordResponse{} + err := client.Do("GET", recordLink, nil, &rec) + if err != nil { + return nil, err + } + + // ignore all records but the types supported by external- + target := extractTarget(rec.Data.RecordType, &rec.Data.RData) + if target == "" { + return nil, nil + } + + result = &endpoint.Endpoint{ + DNSName: rec.Data.FQDN, + RecordTTL: endpoint.TTL(rec.Data.TTL), + RecordType: rec.Data.RecordType, + Target: target, + } + + log.Debugf("Fetched new endpoint for %s: %+v", recordLink, result) + d.Cache.Put(recordLink, result) + return result, nil +} + +func errorOrValue(err error, value interface{}) interface{} { + if err == nil { + return value + } + + return err +} + +// endpointToRecord puts the Target of an Endpoint in the correct field of DataBlock. +// See DataBlock comments for more info +func endpointToRecord(ep *endpoint.Endpoint) *dynect.DataBlock { + result := dynect.DataBlock{} + + if ep.RecordType == endpoint.RecordTypeA { + result.Address = ep.Target + } else if ep.RecordType == endpoint.RecordTypeCNAME { + result.CName = ep.Target + } else if ep.RecordType == endpoint.RecordTypeTXT { + result.TxtData = ep.Target + } + + return &result +} + +// fetchAllRecordLinksInZone list all records in a zone with a single call. Records not matched by the +// DomainFilter are ignored. The response is a list of links that can be fed to dynect.Client.Do() +// directly +func (d *dynProviderState) fetchAllRecordLinksInZone(client *dynect.Client, zone string) ([]string, error) { + var allRecords dynect.AllRecordsResponse + err := client.Do("GET", fmt.Sprintf("AllRecord/%s/", zone), nil, &allRecords) + if err != nil { + return nil, err + } + + return filterAndFixLinks(allRecords.Data, d.DomainFilter), nil +} + +// buildLinkToRecord build a resource link. The symmetry of the dyn API is used to save +// switch-case boilerplate. +// Empty response means the endpoint is not mappable to a records link: either because the fqdn +// is not matched by the domainFilter or it is in the wrong zone +func (d *dynProviderState) buildLinkToRecord(ep *endpoint.Endpoint) string { + if ep == nil { + return "" + } + var matchingZone = "" + for _, zone := range d.ZoneIDFilter.zoneIDs { + if strings.HasSuffix(ep.DNSName, zone) { + matchingZone = zone + break + } + } + + if matchingZone == "" { + fmt.Printf("no zone") + // no matching zone, ignore + return "" + } + + if !d.DomainFilter.Match(ep.DNSName) { + // no matching domain, ignore + return "" + } + + return fmt.Sprintf("%sRecord/%s/%s/", ep.RecordType, matchingZone, ep.DNSName) +} + +// create a dynect client and performs login. You need to clean it up. +// This method also stores the DynAPI version. +// Don't user the dynect.Client.Login() +func (d *dynProviderState) login() (*dynect.Client, error) { + client := dynect.NewClient(d.CustomerName) + + var req = dynect.LoginBlock{ + Username: d.Username, + Password: d.Password, + CustomerName: d.CustomerName} + + var resp dynect.LoginResponse + + err := client.Do("POST", "Session", req, &resp) + if err != nil { + return nil, err + } + + client.Token = resp.Data.Token + + // this is the only change from the original + d.DynVersion = resp.Data.Version + return client, nil +} + +// the zones we are allowed to touch. Currently only exact matches are considered, not all +// zones with the given suffix +func (d *dynProviderState) zones(client *dynect.Client) []string { + return d.ZoneIDFilter.zoneIDs +} + +func (d *dynProviderState) buildRecordRequest(ep *endpoint.Endpoint) (string, *dynect.RecordRequest) { + link := d.buildLinkToRecord(ep) + if link == "" { + return "", nil + } + + record := dynect.RecordRequest{ + TTL: fixMissingTTL(ep.RecordTTL), + RData: *endpointToRecord(ep), + } + return link, &record +} + +// deleteRecord deletes all existing records (CNAME, TXT, A) for the given Endpoint.DNSName with 1 API call +func (d *dynProviderState) deleteRecord(client *dynect.Client, ep *endpoint.Endpoint) error { + link := d.buildLinkToRecord(ep) + if link == "" { + return nil + } + + response := dynect.RecordResponse{} + err := client.Do("DELETE", link, nil, &response) + log.Debugf("Deleting record %s: %+v,", link, errorOrValue(err, &response)) + return err +} + +// replaceRecord replaces all existing records pf the given type for the Endpoint.DNSName with 1 API call +func (d *dynProviderState) replaceRecord(client *dynect.Client, ep *endpoint.Endpoint) error { + link, record := d.buildRecordRequest(ep) + if link == "" { + return nil + } + + response := dynect.RecordResponse{} + err := client.Do("PUT", link, record, &response) + log.Debugf("Replacing record %s: %+v,", link, errorOrValue(err, &response)) + return err +} + +// createRecord creates a single record with 1 API call +func (d *dynProviderState) createRecord(client *dynect.Client, ep *endpoint.Endpoint) error { + link, record := d.buildRecordRequest(ep) + if link == "" { + return nil + } + + response := dynect.RecordResponse{} + err := client.Do("POST", link, record, &response) + log.Debugf("Creating record %s: %+v,", link, errorOrValue(err, &response)) + return err +} + +// commit commits all pending changes. It will always attempt to commit, if there are no +func (d *dynProviderState) commit(client *dynect.Client) error { + errs := []error{} + + for _, zone := range d.zones(client) { + // extra call if in debug mode to fetch pending changes + if log.GetLevel() >= log.DebugLevel { + response := ZoneChangesResponse{} + err := client.Do("GET", fmt.Sprintf("ZoneChanges/%s/", zone), nil, &response) + log.Debugf("Pending changes for zone %s: %+v", zone, errorOrValue(err, &response)) + } + + h, err := os.Hostname() + if err != nil { + h = "unknown-host" + } + notes := fmt.Sprintf("Change by external-dns@%s, DynAPI@%s, %s on %s", + d.AppVersion, + d.DynVersion, + time.Now().Format(time.RFC3339), + h, + ) + + zonePublish := ZonePublishRequest{ + Publish: true, + Notes: notes, + } + + response := ZonePublisResponse{} + err = client.Do("PUT", fmt.Sprintf("Zone/%s/", zone), &zonePublish, &response) + log.Infof("Commiting changes for zone %s: %+v", zone, errorOrValue(err, &response)) + } + + switch len(errs) { + case 0: + return nil + case 1: + return errs[0] + default: + return fmt.Errorf("Multiple errors committing: %+v", errs) + } +} + +// Records makes on average C + 2*Z requests (Z = number of zones): 1 login + 1 fetchAllRecords +// A cache is used to avoid querying for every single record found. C is proportional to the number +// of expired/changed records +func (d *dynProviderState) Records() ([]*endpoint.Endpoint, error) { + client, err := d.login() + if err != nil { + return nil, err + } + defer client.Logout() + + log.Debugf("Using DynAPI@%s", d.DynVersion) + + var result []*endpoint.Endpoint + + zones := d.zones(client) + log.Infof("Zones found: %+v", zones) + for _, zone := range zones { + recordLinks, err := d.fetchAllRecordLinksInZone(client, zone) + if err != nil { + return nil, err + } + + log.Infof("Relevant records found in zone %s: %+v", zone, recordLinks) + for _, link := range recordLinks { + ep, err := d.recordLinkToEndpoint(client, link) + if err != nil { + return nil, err + } + + if ep != nil { + result = append(result, ep) + } + } + } + + return result, nil +} + +// this method does C + 2*Z requests: C=total number of changes, Z = number of +// affected zones (1 login + 1 commit) +func (d *dynProviderState) ApplyChanges(changes *plan.Changes) error { + log.Debugf("Processing chages: %+v", changes) + + if d.DryRun { + log.Infof("Will NOT delete these records: %+v", changes.Delete) + log.Infof("Will NOT create these records: %+v", changes.Create) + log.Infof("Will NOT update these records: %+v", merge(changes.UpdateOld, changes.UpdateNew)) + return nil + } + + client, err := d.login() + if err != nil { + return err + } + defer client.Logout() + + var errs []error + + needsCommit := false + + for _, ep := range changes.Delete { + err := d.deleteRecord(client, ep) + if err != nil { + errs = append(errs, err) + } else { + needsCommit = true + } + } + + for _, ep := range changes.Create { + err := d.createRecord(client, ep) + if err != nil { + errs = append(errs, err) + } else { + needsCommit = true + } + } + + updates := merge(changes.UpdateOld, changes.UpdateNew) + log.Debugf("Updates after merging: %+v", updates) + for _, ep := range updates { + err := d.replaceRecord(client, ep) + if err != nil { + errs = append(errs, err) + } else { + needsCommit = true + } + } + + switch len(errs) { + case 0: + case 1: + return errs[0] + default: + return fmt.Errorf("Multiple errors committing: %+v", errs) + } + + if needsCommit { + return d.commit(client) + } + + return nil +} diff --git a/provider/dyn_test.go b/provider/dyn_test.go new file mode 100644 index 0000000000..6ae0e831ea --- /dev/null +++ b/provider/dyn_test.go @@ -0,0 +1,298 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/nesv/go-dynect/dynect" + "github.com/stretchr/testify/assert" + + "github.com/kubernetes-incubator/external-dns/endpoint" +) + +func TestDynMerge_NoUpdateOnTTL0Changes(t *testing.T) { + updateOld := []*endpoint.Endpoint{ + { + DNSName: "name1", + Target: "target1", + RecordTTL: endpoint.TTL(1), + RecordType: endpoint.RecordTypeA, + }, + { + DNSName: "name2", + Target: "target2", + RecordTTL: endpoint.TTL(1), + RecordType: endpoint.RecordTypeA, + }, + } + + updateNew := []*endpoint.Endpoint{ + { + DNSName: "name1", + Target: "target1", + RecordTTL: endpoint.TTL(0), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Target: "target2", + RecordTTL: endpoint.TTL(0), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + assert.Equal(t, 0, len(merge(updateOld, updateNew))) +} + +func TestDynMerge_UpdateOnTTLChanges(t *testing.T) { + updateOld := []*endpoint.Endpoint{ + { + DNSName: "name1", + Target: "target1", + RecordTTL: endpoint.TTL(1), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Target: "target2", + RecordTTL: endpoint.TTL(1), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + updateNew := []*endpoint.Endpoint{ + { + DNSName: "name1", + Target: "target1", + RecordTTL: endpoint.TTL(77), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Target: "target2", + RecordTTL: endpoint.TTL(10), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + merged := merge(updateOld, updateNew) + assert.Equal(t, 2, len(merged)) + assert.Equal(t, "name1", merged[0].DNSName) +} + +func TestDynMerge_AlwaysUpdateTarget(t *testing.T) { + updateOld := []*endpoint.Endpoint{ + { + DNSName: "name1", + Target: "target1", + RecordTTL: endpoint.TTL(1), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Target: "target2", + RecordTTL: endpoint.TTL(1), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + updateNew := []*endpoint.Endpoint{ + { + DNSName: "name1", + Target: "target1-changed", + RecordTTL: endpoint.TTL(0), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Target: "target2", + RecordTTL: endpoint.TTL(0), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + merged := merge(updateOld, updateNew) + assert.Equal(t, 1, len(merged)) + assert.Equal(t, "target1-changed", merged[0].Target) +} + +func TestDynMerge_NoUpdateIfTTLUnchanged(t *testing.T) { + updateOld := []*endpoint.Endpoint{ + { + DNSName: "name1", + Target: "target1", + RecordTTL: endpoint.TTL(55), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Target: "target2", + RecordTTL: endpoint.TTL(55), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + updateNew := []*endpoint.Endpoint{ + { + DNSName: "name1", + Target: "target1", + RecordTTL: endpoint.TTL(55), + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "name2", + Target: "target2", + RecordTTL: endpoint.TTL(55), + RecordType: endpoint.RecordTypeCNAME, + }, + } + + merged := merge(updateOld, updateNew) + assert.Equal(t, 0, len(merged)) +} + +func TestDyn_extractTarget(t *testing.T) { + tests := []struct { + recordType string + block *dynect.DataBlock + target string + }{ + {"A", &dynect.DataBlock{Address: "address"}, "address"}, + {"CNAME", &dynect.DataBlock{CName: "name."}, "name"}, // note trailing dot is trimmed for CNAMEs + {"TXT", &dynect.DataBlock{TxtData: "text."}, "text."}, + } + + for _, tc := range tests { + assert.Equal(t, tc.target, extractTarget(tc.recordType, tc.block)) + } +} + +func TestDyn_endpointToRecord(t *testing.T) { + tests := []struct { + ep *endpoint.Endpoint + extractor func(*dynect.DataBlock) string + }{ + {endpoint.NewEndpoint("address", "the-target", "A"), func(b *dynect.DataBlock) string { return b.Address }}, + {endpoint.NewEndpoint("cname", "the-target", "CNAME"), func(b *dynect.DataBlock) string { return b.CName }}, + {endpoint.NewEndpoint("text", "the-target", "TXT"), func(b *dynect.DataBlock) string { return b.TxtData }}, + } + + for _, tc := range tests { + block := endpointToRecord(tc.ep) + assert.Equal(t, "the-target", tc.extractor(block)) + } +} + +func TestDyn_buildLinkToRecord(t *testing.T) { + provider := &dynProviderState{ + DynConfig: DynConfig{ + ZoneIDFilter: NewZoneIDFilter([]string{"example.com"}), + DomainFilter: NewDomainFilter([]string{"the-target.example.com"}), + }, + } + + tests := []struct { + ep *endpoint.Endpoint + link string + }{ + {endpoint.NewEndpoint("sub.the-target.example.com", "address", "A"), "ARecord/example.com/sub.the-target.example.com/"}, + {endpoint.NewEndpoint("the-target.example.com", "cname", "CNAME"), "CNAMERecord/example.com/the-target.example.com/"}, + {endpoint.NewEndpoint("the-target.example.com", "text", "TXT"), "TXTRecord/example.com/the-target.example.com/"}, + {endpoint.NewEndpoint("the-target.google.com", "text", "TXT"), ""}, + {endpoint.NewEndpoint("mail.example.com", "text", "TXT"), ""}, + {nil, ""}, + } + + for _, tc := range tests { + assert.Equal(t, tc.link, provider.buildLinkToRecord(tc.ep)) + } +} + +func TestDyn_errorOrValue(t *testing.T) { + e := errors.New("an error") + val := "value" + assert.Equal(t, e, errorOrValue(e, val)) + assert.Equal(t, val, errorOrValue(nil, val)) +} + +func TestDyn_filterAndFixLinks(t *testing.T) { + links := []string{ + "/REST/ARecord/example.com/the-target.example.com/", + "/REST/ARecord/example.com/the-target.google.com/", + "/REST/TXTRecord/example.com/the-target.example.com/", + "/REST/TXTRecord/example.com/the-target.google.com/", + "/REST/CNAMERecord/example.com/the-target.google.com/", + "/REST/CNAMERecord/example.com/the-target.example.com/", + "/REST/NSRecord/example.com/the-target.google.com/", + "/REST/NSRecord/example.com/the-target.example.com/", + } + filter := NewDomainFilter([]string{"example.com"}) + result := filterAndFixLinks(links, filter) + + // should skip non-example.com records and NS records too + assert.Equal(t, 3, len(result)) + assert.Equal(t, "ARecord/example.com/the-target.example.com/", result[0]) + assert.Equal(t, "TXTRecord/example.com/the-target.example.com/", result[1]) + assert.Equal(t, "CNAMERecord/example.com/the-target.example.com/", result[2]) +} + +func TestDyn_fixMissingTTL(t *testing.T) { + assert.Equal(t, fmt.Sprintf("%v", dynDefaultTTL), fixMissingTTL(endpoint.TTL(0))) + + // nothing to fix + assert.Equal(t, "111", fixMissingTTL(endpoint.TTL(111))) +} + +func TestDyn_cachePut(t *testing.T) { + c := cache{ + contents: make(map[string]*entry), + } + + c.Put("link", &endpoint.Endpoint{ + DNSName: "name", + Target: "target", + RecordTTL: endpoint.TTL(10000), + RecordType: "A", + }) + + found := c.Get("link") + assert.NotNil(t, found) +} + +func TestDyn_cachePutExpired(t *testing.T) { + c := cache{ + contents: make(map[string]*entry), + } + + c.Put("link", &endpoint.Endpoint{ + DNSName: "name", + Target: "target", + RecordTTL: endpoint.TTL(0), + RecordType: "A", + }) + + time.Sleep(2 * time.Second) + + found := c.Get("link") + assert.Nil(t, found) + + assert.Nil(t, c.Get("no-such-records")) +} diff --git a/vendor/github.com/nesv/go-dynect/CHANGELOG.md b/vendor/github.com/nesv/go-dynect/CHANGELOG.md new file mode 100644 index 0000000000..d909754878 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/CHANGELOG.md @@ -0,0 +1,102 @@ +# Changelog + +## Tue Jan 9 2018 - 0.6.0 + +- use VCR and fixtures for tests +- test ConvenientClient operations +- add support for zone create/delete operations + +## Wed Aug 23 2017 - 0.5.3 + +- BUG-FIX: don't prepend dot for record with FQDN of Zone name + +## Fri Aug 18 2017 - 0.5.2 + +- Handle errors reading response body in verbose mode (PR#20) + +## Mon Jun 5 2017 - 0.5.1 + +- Update CHANGELOG + +## Mon Jun 5 2017 - 0.5.0 + +- Add support for ALIAS, MX, NS, and SOA records, to the ConvenientClient + (PR#17) + +## Mon Jun 5 2017 - 0.4.1 + +- Handle rate limit errors + +## Mon Jun 5 2017 - 0.4.0 + +- Fix nil-transport issue with the ConvenientClinent (PR#16) + +## Fri Apr 21 2017 - 0.3.1 + +- Proxy support configurable with HTTP(S)_PROXY env variables +- BACKPORT: Handle rate limit errors + +## Thu Sep 22 2016 - 0.3.0 + +- Verbose mode prints full url +- Handle Job redirections +- Support for unknown Content-Length +- Addition of ConvenientClient +- Support for Traffic Director (DSF) service + +- BUGFIX: Don't override global log prefix + +## Fri Nov 15 2013 - 0.2.0 + +- Fixed some struct field types +- Modified some of the tests +- Felt like it deserved a minor version bump + +## Thu Nov 14 2013 - 0.1.9 + +- If verbosity is enabled, any unmarshaling errors will print the complete + response body out, via logger + +## Thu Nov 14 2013 - 0.1.8 + +## Wed Nov 13 2013 - 0.1.7 + +- Fixed a bug where empty request bodies would result in the API service + responding with a 400 Bad Request +- Added some proper tests + +## Wed Nov 13 2013 - 0.1.6 + +- Added a "verbose" mode to the client + +## Tue Nov 12 2013 - 0.1.5 + +- Bug fixes + - Logic bug in the *Client.Do() function, where it would not allow the + POST /Session call if the client was logged out (POST /Session is used for + logging in) + +## Tue Nov 12 2013 - 0.1.4 + +- Includes 0.1.3 +- Bug fixes +- Testing laid out, but there is not much there, as of right now + +## Tue Nov 12 2013 - 0.1.2 + +- Bug fixes + +## Tue Nov 12 2013 - 0.1.1 + +- Added structs for zone responses + +## Tue Nov 12 2013 - 0.1.0 + +- Initial release +- The base client is complete; it will allow you to establish a session, + terminate a session, and issue requests to the DynECT REST API endpoints +- TODO + - Structs for marshaling and unmarshaling requests and responses still need + to be done, as the current set of provided struct is all that is needed + to be able to log in and create a session + - More structs will be added on an "as I need them" basis diff --git a/vendor/github.com/nesv/go-dynect/LICENSE.md b/vendor/github.com/nesv/go-dynect/LICENSE.md new file mode 100644 index 0000000000..9f66f66053 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Nick Saika + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/nesv/go-dynect/README.md b/vendor/github.com/nesv/go-dynect/README.md new file mode 100644 index 0000000000..c7c0e48e56 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/README.md @@ -0,0 +1,43 @@ +# go-dynect + +A DynECT REST client for the Go programming language. + +## Installation + + $ go get github.com/nesv/go-dynect/dynect + +## Usage + + package main + + import ( + "github.com/nesv/go-dynect/dynect" + "log" + ) + + func main() { + client := dynect.NewClient("my-dyn-customer-name") + err := client.Login("my-dyn-username", "my-dyn-password") + if err != nil { + log.Fatal(err) + } + + defer func() { + err := client.Logout() + if err != nil { + log.Fatal(err) + } + }() + + // Make a request to the API, to get a list of all, managed DNS zones + var response dynect.ZonesResponse + if err := client.Do("GET", "Zone", nil, &response); err != nil { + log.Println(err) + } + + for _, zone := range response.Data { + log.Println("Zone", zone) + } + } + +More to come! diff --git a/vendor/github.com/nesv/go-dynect/dynect/client.go b/vendor/github.com/nesv/go-dynect/dynect/client.go new file mode 100644 index 0000000000..51d534d89b --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/client.go @@ -0,0 +1,281 @@ +package dynect + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + "time" +) + +const ( + DynAPIPrefix = "https://api.dynect.net/REST" +) + +var ( + PollingInterval = 1 * time.Second + ErrPromotedToJob = errors.New("promoted to job") + ErrRateLimited = errors.New("too many requests") +) + +// handleJobRedirect overrides the net/http.DefaultClient's redirection policy +// function. +// +// This function will set the Content-Type, and Auth-Token headers, so that we +// don't get an error back from the API. +func handleJobRedirect(req *http.Request, via []*http.Request) error { + // Set the Content-Type header. + req.Header.Set("Content-Type", "application/json") + + // Now, try and divine the Auth-Token header's value from previous + // requests. + for _, r := range via { + if authHdr := r.Header.Get("Auth-Token"); authHdr != "" { + req.Header.Set("Auth-Token", authHdr) + return nil + } + } + return fmt.Errorf("failed to set Auth-Token header from previous requests") +} + +// A client for use with DynECT's REST API. +type Client struct { + Token string + CustomerName string + Transport http.RoundTripper + verbose bool +} + +// Creates a new Httpclient. +func NewClient(customerName string) *Client { + return &Client{ + CustomerName: customerName, + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, + } +} + +// Sets the transport for the client. +func (c *Client) SetTransport(t http.RoundTripper) { + c.Transport = t +} + +// Enable, or disable verbose output from the client. +// +// This will enable (or disable) logging messages that explain what the client +// is about to do, like the endpoint it is about to make a request to. If the +// request fails with an unexpected HTTP response code, then the response body +// will be logged out, as well. +func (c *Client) Verbose(p bool) { + c.verbose = p +} + +// Establishes a new session with the DynECT API. +func (c *Client) Login(username, password string) error { + var req = LoginBlock{ + Username: username, + Password: password, + CustomerName: c.CustomerName} + + var resp LoginResponse + + err := c.Do("POST", "Session", req, &resp) + if err != nil { + return err + } + + c.Token = resp.Data.Token + return nil +} + +func (c *Client) LoggedIn() bool { + return len(c.Token) > 0 +} + +func (c *Client) Logout() error { + return c.Do("DELETE", "Session", nil, nil) +} + +// newRequest creates a new *http.Request, and sets the following headers: +// +func (c *Client) newRequest(method, urlStr string, data []byte) (*http.Request, error) { + var r *http.Request + var err error + + if data != nil { + r, err = http.NewRequest(method, urlStr, bytes.NewReader(data)) + } else { + r, err = http.NewRequest(method, urlStr, nil) + } + + r.Header.Set("Auth-Token", c.Token) + r.Header.Set("Content-Type", "application/json") + + return r, err +} + +func (c *Client) Do(method, endpoint string, requestData, responseData interface{}) error { + // Throw an error if the user tries to make a request if the client is + // logged out/unauthenticated, but make an exemption for when the + // caller is trying to log in. + if !c.LoggedIn() && method != "POST" && endpoint != "Session" { + return errors.New("Will not perform request; client is closed") + } + + var err error + + // Marshal the request data into a byte slice. + if c.verbose { + log.Println("dynect: marshaling request data") + } + var js []byte + if requestData != nil { + js, err = json.Marshal(requestData) + } else { + js = []byte("") + } + if err != nil { + return err + } + + urlStr := fmt.Sprintf("%s/%s", DynAPIPrefix, endpoint) + + // Create a new http.Request. + req, err := c.newRequest(method, urlStr, js) + if err != nil { + return err + } + + if c.verbose { + log.Printf("Making %s request to %q", method, urlStr) + } + + var resp *http.Response + resp, err = c.Transport.RoundTrip(req) + + if err != nil { + if c.verbose { + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + log.Printf("%s", string(respBody)) + } + return err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case 200: + if resp.ContentLength == 0 { + // Zero-length content body? + log.Println("dynect: warning: zero-length response body; skipping decoding of response") + return nil + } + + //dec := json.NewDecoder(resp.Body) + text, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("Could not read response body") + } + if err := json.Unmarshal(text, &responseData); err != nil { + return fmt.Errorf("Error unmarshalling response:", err) + } + + return nil + + case 307: + // Handle the temporary redirect, which should point to a + // /REST/Jobs endpoint. + loc := resp.Header.Get("Location") + log.Println("dynect: request is taking too long to complete: redirecting to", loc) + + // Going in to this blind, the documentation says that it will + // return a URI when promoting a long-running request to a + // job. + // + // Since a URL is technically a URI, we should do some checks + // on the returned URI to sanitize it, and make sure that it is + // in the format we would like it to be. + if strings.HasPrefix(loc, "/REST/") { + loc = strings.TrimLeft(loc, "/REST/") + } + if !strings.HasPrefix(loc, DynAPIPrefix) { + loc = fmt.Sprintf("%s/%s", DynAPIPrefix, loc) + } + + log.Println("Fetching location:", loc) + + // Generate a new request. + req, err := c.newRequest("GET", loc, nil) + if err != nil { + return err + } + + var jobData JobData + + // Poll the API endpoint, until we get a response back. + for { + select { + case <-time.After(PollingInterval): + resp, err := c.Transport.RoundTrip(req) + if err != nil { + return err + } + defer resp.Body.Close() + + text, err := ioutil.ReadAll(resp.Body) + //log.Println(string(text)) + if err != nil { + return fmt.Errorf("Could not read response body:", err) + } + if err := json.Unmarshal(text, &jobData); err != nil { + return fmt.Errorf("failed to decode job response body:", err) + } + + // Check to see the status of the job. + // + // If it is "incomplete", loop around again. + // + // Should the job's status be "success", then + // return the data, business-as-usual. + // + // TODO(nesv): Figure out what to do in the + // event of a "failure" job status. + + switch jobData.Status { + case "incomplete": + continue + case "success": + if err := json.Unmarshal(text, &responseData); err != nil { + return fmt.Errorf("failed to decode response body:", err) + } + return nil + case "failure": + return fmt.Errorf("request failed: %v", jobData.Messages) + } + } + } + + return nil + + case 429: + return ErrRateLimited + } + + // If we got here, this means that the client does not know how to + // interpret the response, and it should just error out. + reason, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read in response body") + } + return fmt.Errorf("server responded with %v: %v", + resp.Status, + string(reason)) +} diff --git a/vendor/github.com/nesv/go-dynect/dynect/client_test.go b/vendor/github.com/nesv/go-dynect/dynect/client_test.go new file mode 100644 index 0000000000..8aafd46c80 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/client_test.go @@ -0,0 +1,123 @@ +package dynect + +import ( + "log" + "os" + "strings" + "testing" + + "github.com/dnaeon/go-vcr/recorder" +) + +var ( + DynCustomerName string + DynUsername string + DynPassword string + testZone string +) + +func getenv(key, defaultValue string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return defaultValue +} + +func init() { + DynCustomerName = getenv("DYNECT_CUSTOMER_NAME", "go-dynect") + DynUsername = getenv("DYNECT_USER_NAME", "dynect-user") + DynPassword = getenv("DYNECT_PASSWORD", "p@55w0rd") + testZone = getenv("DYNECT_TEST_ZONE", "go-dynect.test") +} + +// test helper to begin recording or playback of vcr cassette +func withCassette(cassetteName string, f func(*recorder.Recorder)) { + r, err := recorder.New(cassetteName) + if err != nil { + log.Fatal(err) + } + + defer r.Stop() + + f(r) +} + +// test helper to setup client with vcr cassette +func withClient(cassetteName string, f func(*Client)) { + withCassette(cassetteName, func(r *recorder.Recorder) { + c := NewClient(DynCustomerName) + c.SetTransport(r) + c.Verbose(true) + + f(c) + }) +} + +// test helper to setup authenticated client with vcr cassette +func testWithClientSession(cassetteName string, t *testing.T, f func(*Client)) { + withClient(cassetteName, func(c *Client) { + if err := c.Login(DynUsername, DynPassword); err != nil { + t.Fatal(err) + } + + defer func() { + if err := c.Logout(); err != nil { + t.Error(err) + } + }() + + f(c) + }) +} + +func TestLoginLogout(t *testing.T) { + withClient("fixtures/login_logout", func(c *Client) { + if err := c.Login(DynUsername, DynPassword); err != nil { + t.Error(err) + } + + if err := c.Logout(); err != nil { + t.Error(err) + } + }) +} + +func TestZonesRequest(t *testing.T) { + testWithClientSession("fixtures/zones_request", t, func(c *Client) { + var resp ZonesResponse + + if err := c.Do("GET", "Zone", nil, &resp); err != nil { + t.Fatal(err) + } + + nresults := len(resp.Data) + for i, zone := range resp.Data { + parts := strings.Split(zone, "/") + t.Logf("(%d/%d) %q", i+1, nresults, parts[len(parts)-2]) + } + }) +} + +func TestFetchingAllZoneRecords(t *testing.T) { + testWithClientSession("fixtures/fetching_all_zone_records", t, func(c *Client) { + var resp AllRecordsResponse + + if err := c.Do("GET", "AllRecord/"+testZone, nil, &resp); err != nil { + t.Error(err) + } + + for _, zr := range resp.Data { + parts := strings.Split(zr, "/") + uri := strings.Join(parts[2:], "/") + t.Log(uri) + + var record RecordResponse + + if err := c.Do("GET", uri, nil, &record); err != nil { + t.Fatal(err) + } + + t.Log("OK") + } + }) +} diff --git a/vendor/github.com/nesv/go-dynect/dynect/convenient_client.go b/vendor/github.com/nesv/go-dynect/dynect/convenient_client.go new file mode 100644 index 0000000000..e3f06fd50e --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/convenient_client.go @@ -0,0 +1,242 @@ +package dynect + +import ( + "fmt" + "log" + "net/http" + "strconv" + "strings" +) + +// ConvenientClient A client with extra helper methods for common actions +type ConvenientClient struct { + Client +} + +// NewConvenientClient Creates a new ConvenientClient +func NewConvenientClient(customerName string) *ConvenientClient { + return &ConvenientClient{ + Client{ + CustomerName: customerName, + Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, + }} +} + +// CreateZone method to create a zone +func (c *ConvenientClient) CreateZone(zone, rname, serialStyle, ttl string) error { + url := fmt.Sprintf("Zone/%s/", zone) + data := &CreateZoneBlock{ + RName: rname, + SerialStyle: serialStyle, + TTL: ttl, + } + + if err := c.Do("POST", url, data, nil); err != nil { + return fmt.Errorf("Failed to create zone: %s", err) + } + + return nil +} + +// GetZone method to read a zone +func (c *ConvenientClient) GetZone(z *Zone) error { + url := fmt.Sprintf("Zone/%s", z.Zone) + data := &ZoneResponse{} + + if err := c.Do("GET", url, nil, data); err != nil { + return fmt.Errorf("Failed to get zone: %s", err) + } + + z.Serial = strconv.Itoa(data.Data.Serial) + z.SerialStyle = data.Data.SerialStyle + z.Zone = data.Data.Zone + z.Type = data.Data.ZoneType + + return nil +} + +// PublishZone Publish a specific zone and the changes for the current session +func (c *ConvenientClient) PublishZone(zone string) error { + url := fmt.Sprintf("Zone/%s", zone) + data := &PublishZoneBlock{ + Publish: true, + } + + if err := c.Do("PUT", url, data, nil); err != nil { + return fmt.Errorf("Failed to publish zone: %s", err) + } + + return nil +} + +// DeleteZoneNode method to delete everything in a zone +func (c *ConvenientClient) DeleteZoneNode(zone string) error { + parentZone := strings.Join(strings.Split(zone, ".")[1:], ".") + url := fmt.Sprintf("Node/%s/%s", parentZone, zone) + + if err := c.Do("DELETE", url, nil, nil); err != nil { + return fmt.Errorf("Failed to delete zone node: %s", err) + } + + return nil +} + +// DeleteZone method to delete a zone +func (c *ConvenientClient) DeleteZone(zone string) error { + url := fmt.Sprintf("Zone/%s/", zone) + + if err := c.Do("DELETE", url, nil, nil); err != nil { + return fmt.Errorf("Failed to delete zone: %s", err) + } + + return nil +} + +// GetRecordID finds the dns record ID by fetching all records for a FQDN +func (c *ConvenientClient) GetRecordID(record *Record) error { + finalID := "" + url := fmt.Sprintf("AllRecord/%s/%s", record.Zone, record.FQDN) + var records AllRecordsResponse + err := c.Do("GET", url, nil, &records) + if err != nil { + return fmt.Errorf("Failed to find Dyn record id: %s", err) + } + for _, recordURL := range records.Data { + id := strings.TrimPrefix(recordURL, fmt.Sprintf("/REST/%sRecord/%s/%s/", record.Type, record.Zone, record.FQDN)) + if !strings.Contains(id, "/") && id != "" { + finalID = id + log.Printf("[INFO] Found Dyn record ID: %s", id) + } + } + if finalID == "" { + return fmt.Errorf("Failed to find Dyn record id!") + } + + record.ID = finalID + return nil +} + +// CreateRecord Method to create a DNS record +func (c *ConvenientClient) CreateRecord(record *Record) error { + if record.FQDN == "" && record.Name == "" { + record.FQDN = record.Zone + } else if record.FQDN == "" { + record.FQDN = fmt.Sprintf("%s.%s", record.Name, record.Zone) + } + rdata, err := buildRData(record) + if err != nil { + return fmt.Errorf("Failed to create Dyn RData: %s", err) + } + url := fmt.Sprintf("%sRecord/%s/%s", record.Type, record.Zone, record.FQDN) + data := &RecordRequest{ + RData: rdata, + TTL: record.TTL, + } + return c.Do("POST", url, data, nil) +} + +// UpdateRecord Method to update a DNS record +func (c *ConvenientClient) UpdateRecord(record *Record) error { + if record.FQDN == "" { + record.FQDN = fmt.Sprintf("%s.%s", record.Name, record.Zone) + } + rdata, err := buildRData(record) + if err != nil { + return fmt.Errorf("Failed to create Dyn RData: %s", err) + } + url := fmt.Sprintf("%sRecord/%s/%s/%s", record.Type, record.Zone, record.FQDN, record.ID) + data := &RecordRequest{ + RData: rdata, + TTL: record.TTL, + } + return c.Do("PUT", url, data, nil) +} + +// DeleteRecord Method to delete a DNS record +func (c *ConvenientClient) DeleteRecord(record *Record) error { + if record.FQDN == "" { + record.FQDN = fmt.Sprintf("%s.%s", record.Name, record.Zone) + } + // safety check that we have an ID, otherwise we could accidentally delete everything + if record.ID == "" { + return fmt.Errorf("No ID found! We can't continue!") + } + url := fmt.Sprintf("%sRecord/%s/%s/%s", record.Type, record.Zone, record.FQDN, record.ID) + return c.Do("DELETE", url, nil, nil) +} + +// GetRecord Method to get record details +func (c *ConvenientClient) GetRecord(record *Record) error { + url := fmt.Sprintf("%sRecord/%s/%s/%s", record.Type, record.Zone, record.FQDN, record.ID) + var rec RecordResponse + err := c.Do("GET", url, nil, &rec) + if err != nil { + return err + } + + record.Zone = rec.Data.Zone + record.FQDN = rec.Data.FQDN + record.Name = strings.TrimSuffix(rec.Data.FQDN, "."+rec.Data.Zone) + record.Type = rec.Data.RecordType + record.TTL = strconv.Itoa(rec.Data.TTL) + + switch rec.Data.RecordType { + case "A", "AAAA": + record.Value = rec.Data.RData.Address + case "ALIAS": + record.Value = rec.Data.RData.Alias + case "CNAME": + record.Value = rec.Data.RData.CName + case "MX": + record.Value = fmt.Sprintf("%d %s", rec.Data.RData.Preference, rec.Data.RData.Exchange) + case "NS": + record.Value = rec.Data.RData.NSDName + case "SOA": + record.Value = rec.Data.RData.RName + case "TXT", "SPF": + record.Value = rec.Data.RData.TxtData + default: + fmt.Println("unknown response", rec) + return fmt.Errorf("Invalid Dyn record type: %s", rec.Data.RecordType) + } + + return nil +} + +func buildRData(r *Record) (DataBlock, error) { + var rdata DataBlock + + switch r.Type { + case "A", "AAAA": + rdata = DataBlock{ + Address: r.Value, + } + case "ALIAS": + rdata = DataBlock{ + Alias: r.Value, + } + case "CNAME": + rdata = DataBlock{ + CName: r.Value, + } + case "MX": + rdata = DataBlock{} + fmt.Sscanf(r.Value, "%d %s", &rdata.Preference, &rdata.Exchange) + case "NS": + rdata = DataBlock{ + NSDName: r.Value, + } + case "SOA": + rdata = DataBlock{ + RName: r.Value, + } + case "TXT", "SPF": + rdata = DataBlock{ + TxtData: r.Value, + } + default: + return rdata, fmt.Errorf("Invalid Dyn record type: %s", r.Type) + } + + return rdata, nil +} diff --git a/vendor/github.com/nesv/go-dynect/dynect/convenient_client_test.go b/vendor/github.com/nesv/go-dynect/dynect/convenient_client_test.go new file mode 100644 index 0000000000..9b29d0b272 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/convenient_client_test.go @@ -0,0 +1,241 @@ +package dynect + +import ( + "fmt" + "strconv" + "strings" + "testing" + + "github.com/dnaeon/go-vcr/recorder" +) + +// test helper to setup convenient client with vcr cassette +func withConvenientClient(cassetteName string, f func(*ConvenientClient)) { + withCassette(cassetteName, func(r *recorder.Recorder) { + c := NewConvenientClient(DynCustomerName) + c.SetTransport(r) + c.Verbose(true) + + f(c) + }) +} + +// test helper to setup authenticated convenient client with vcr cassette +func testWithConvenientClientSession(cassetteName string, t *testing.T, f func(*ConvenientClient)) { + withConvenientClient(cassetteName, func(c *ConvenientClient) { + if err := c.Login(DynUsername, DynPassword); err != nil { + t.Error(err) + } + + defer func() { + if err := c.Logout(); err != nil { + t.Error(err) + } + }() + + f(c) + }) +} + +func TestConvenientLoginLogout(t *testing.T) { + withConvenientClient("fixtures/login_logout", func(c *ConvenientClient) { + if err := c.Login(DynUsername, DynPassword); err != nil { + t.Error(err) + } + + if err := c.Logout(); err != nil { + t.Error(err) + } + }) +} + +func TestConvenientGetA(t *testing.T) { + testWithConvenientClientSession("fixtures/convenient_get_a", t, func(c *ConvenientClient) { + actual := Record{ + Zone: testZone, + Type: "A", + FQDN: "foobar." + testZone, + } + + if err := c.GetRecordID(&actual); err != nil { + t.Fatal(err) + } + + if err := c.GetRecord(&actual); err != nil { + t.Fatal(err) + } + + if actual.Value != "10.9.8.7" { + t.Fatalf("Incorrect value %q for %q (expected %q)", actual.Value, actual.FQDN, "foobar.go-dynect.test.") + } + + t.Log("OK") + }) +} + +func TestConvenientGetANotFound(t *testing.T) { + testWithConvenientClientSession("fixtures/convenient_get_a_not_found", t, func(c *ConvenientClient) { + actual := Record{ + Zone: testZone, + Type: "A", + FQDN: "unknown." + testZone, + } + + if err := c.GetRecordID(&actual); err == nil { + t.Fatalf("Expected error getting %q", actual.FQDN) + } else if !strings.HasPrefix(err.Error(), "Failed to find Dyn record id:") { + t.Fatalf("Expected error %q for %q (actual error %q)", "Failed to find Dyn record id:", actual.FQDN, err.Error()) + } + + t.Log("OK") + }) +} + +func TestConvenientGetCNAME(t *testing.T) { + testWithConvenientClientSession("fixtures/convenient_get_cname", t, func(c *ConvenientClient) { + actual := Record{ + Zone: testZone, + Type: "CNAME", + FQDN: "foo." + testZone, + } + + if err := c.GetRecordID(&actual); err != nil { + t.Fatal(err) + } + + if err := c.GetRecord(&actual); err != nil { + t.Fatal(err) + } + + if actual.Value != "foobar.go-dynect.test." { + t.Fatalf("Incorrect value %q (expected %q)", actual.Value, "foobar.go-dynect.test.") + } + + t.Log("OK") + }) +} + +func TestConvenientCreateMX(t *testing.T) { + testWithConvenientClientSession("fixtures/convenient_create_mx", t, func(c *ConvenientClient) { + record := Record{ + Zone: testZone, + Type: "MX", + Value: "123 mx.example.com.", + TTL: "12345", + } + + if err := c.CreateRecord(&record); err != nil { + t.Fatal(err) + } + + if err := c.PublishZone(testZone); err != nil { + t.Fatal(err) + } + + if err := c.GetRecordID(&record); err != nil { + t.Fatal(err) + } + + if err := c.GetRecord(&record); err != nil { + t.Fatal(err) + } + + if record.FQDN != testZone { + t.Fatalf("Expected FQDN %q (actual %q)", testZone, record.FQDN) + } + + id, err := strconv.Atoi(record.ID) + if err != nil || id <= 0 { + t.Fatalf("Expected ID to be positive integer (actual %q)", record.ID) + } + + ttl, err := strconv.Atoi(record.TTL) + if err != nil || ttl != 12345 { + t.Fatalf("Expected ID to be 12345 (actual %q)", record.TTL) + } + + t.Log("OK") + }) +} + +func TestConvenientCreateZone(t *testing.T) { + testWithConvenientClientSession("fixtures/convenient_create_zone", t, func(c *ConvenientClient) { + subZone := fmt.Sprintf("subzone.%s", testZone) + + if err := c.CreateZone(subZone, "admin@example.com", "day", "1800"); err != nil { + t.Fatal(err) + } + + if err := c.PublishZone(subZone); err != nil { + t.Fatal(err) + } + + z := &Zone{Zone: subZone} + + if err := c.GetZone(z); err != nil { + t.Fatal(err) + } + + if z.Zone != subZone { + t.Fatalf("Expected Zone of %q (actual %q)", subZone, z.Zone) + } + + if z.Type != "Primary" { + t.Fatalf("Expected Zone Type of %q (actual %q)", "Primary", z.Type) + } + + if z.SerialStyle != "day" { + t.Fatalf("Expected SerialStyle of %q (actual %q)", "day", z.SerialStyle) + } + + if z.Serial == "" { + t.Fatalf("Expected non-empty Serial (actual %q)", z.Serial) + } + + t.Log("OK") + }) +} + +func TestConvenientDeleteZone(t *testing.T) { + testWithConvenientClientSession("fixtures/convenient_delete_zone", t, func(c *ConvenientClient) { + subZone := fmt.Sprintf("zone-%s", testZone) + + if err := c.DeleteZone(subZone); err != nil { + t.Fatal(err) + } + + z := &Zone{Zone: subZone} + + if err := c.GetZone(z); err == nil { + t.Fatalf("Zone %q not deleted", subZone) + } + + t.Log("OK") + }) +} + +func TestConvenientDeleteSubZone(t *testing.T) { + testWithConvenientClientSession("fixtures/convenient_delete_sub_zone", t, func(c *ConvenientClient) { + subZone := fmt.Sprintf("subzone.%s", testZone) + + if err := c.DeleteZone(subZone); err != nil { + t.Fatal(err) + } + + if err := c.DeleteZoneNode(subZone); err != nil { + t.Fatal(err) + } + + if err := c.PublishZone(testZone); err != nil { + t.Fatal(err) + } + + z := &Zone{Zone: subZone} + + if err := c.GetZone(z); err == nil { + t.Fatalf("Zone %q not deleted", subZone) + } + + t.Log("OK") + }) +} diff --git a/vendor/github.com/nesv/go-dynect/dynect/dsfs.go b/vendor/github.com/nesv/go-dynect/dynect/dsfs.go new file mode 100644 index 0000000000..e099f4112f --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/dsfs.go @@ -0,0 +1,117 @@ +package dynect + +// DSFSResponse is used for holding the data returned by a call to +// "https://api.dynect.net/REST/DSF/" with 'detail: Y'. +type AllDSFDetailedResponse struct { + ResponseBlock + Data []DSFService `json:"data"` +} + +// DSFResponse is used for holding the data returned by a call to +// "https://api.dynect.net/REST/DSF/SERVICE_ID". +type DSFResponse struct { + ResponseBlock + Data DSFService `json:"data"` +} + +// Type DSFService is used as a nested struct, which holds the data for a +// DSF Service returned by a call to "https://api.dynect.net/REST/DSF/SERVICE_ID". +type DSFService struct { + ID string `json:"service_id"` + Label string `json:"label"` + Active string `json:"active"` + TTL string `json:"ttl"` + PendingChange string `json:"pending_change"` + Notifiers []Notifier `json:"notifiers"` + Nodes []DSFNode `json:"nodes"` + Rulesets []DSFRuleset `json:"rulesets"` +} + +type DSFRuleset struct { + ID string `json:"dsf_ruleset_id` + Label string `json:"label"` + CriteriaType string `json:"criteria_type"` + Criteria interface{} `json:"criteria"` + Ordering string `json:"ordering"` + Eligible string `json:"eligible"` + PendingChange string `json:"pending_change"` + ResponsePools []DSFResponsePool `json:"response_pools"` +} + +type DSFResponsePool struct { + ID string `json:"dsf_response_pool_id"` + Label string `json:"label"` + Automation string `json:"automation"` + CoreSetCount string `json:"core_set_count"` + Eligible string `json:"eligible"` + PendingChange string `json:"pending_change"` + RsChains []DSFRecordSetChain `json:"rs_chains"` + Rulesets []DSFRuleset `json:"rulesets"` + Status string `json:"status"` + LastMonitored string `json:"last_monitored"` + Notifier string `json:"notifier"` +} + +type DSFRecordSetChain struct { + ID string `json:"dsf_record_set_failover_chain_id"` + Status string `json:"status"` + Core string `json:"core"` + Label string `json:"label"` + DSFResponsePoolID string `json:"dsf_response_pool_id"` + DSFServiceID string `json:"service_id"` + PendingChange string `json:"pending_change"` + DSFRecordSets []DSFRecordSet `json:"record_sets"` +} + +type DSFRecordSet struct { + Status string `json:"status"` + Eligible string `json:"eligible"` + ID string `json:"dsf_record_set_id"` + MonitorID string `json:"dsf_monitor_id"` + Label string `json:"label"` + TroubleCount string `json:"trouble_count"` + Records []DSFRecord `json:"records"` + FailCount string `json:"fail_count"` + TorpidityMax string `json:"torpidity_max"` + TTLDerived string `json:"ttl_derived"` + LastMonitored string `json:"last_monitored"` + TTL string `json:"ttl"` + ServiceID string `json:"service_id"` + ServeCount string `json:"serve_count"` + Automation string `json:"automation"` + PendingChange string `json:"pending_change"` +} + +type DSFRecord struct { + Status string `json:"status"` + Endpoints []string `json:"endpoints"` + RDataClass string `json:"rdata_class"` + Weight int `json:"weight"` + Eligible string `json:"eligible"` + ID string `json:"dsf_record_id"` + DSFRecordSetID string `json:"dsf_record_set_id"` + //RData interface{} `json:"rdata"` + EndpointUpCount int `json:"endpoint_up_count"` + Label string `json:"label"` + MasterLine string `json:"master_line"` + Torpidity int `json:"torpidity"` + LastMonitored int `json:"last_monitored"` + TTL string `json:"ttl"` + DSFServiceID string `json:"service_id"` + PendingChange string `json:"pending_change"` + Automation string `json:"automation"` + ReponseTime int `json:"response_time"` + Publish string `json:"publish",omit_empty` +} + +type DSFNode struct { + Zone string `json:"zone"` + FQDN string `json:"fqdn"` +} + +type Notifier struct { + ID int `json:"notifier_id"` + Label string `json:"label"` + Recipients string `json:"recipients"` + Active string `json:"active"` +} diff --git a/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_create_mx.yaml b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_create_mx.yaml new file mode 100644 index 0000000000..e47e9f1e37 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_create_mx.yaml @@ -0,0 +1,161 @@ +--- +version: 1 +rwmutex: {} +interactions: +- request: + body: '{"user_name":"dynect-user","password":"p@55w0rd","customer_name":"go-dynect"}' + form: {} + headers: + Auth-Token: + - "" + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: POST + response: + body: '{"status": "success", "data": {"token": "5Trj0G1M2B0g1t1IY09yFwGpn31tjWNRNU81RhYHaUp6kxGa3UVK5F9hQqlZBu9SNLxkj6cAk6q93ndW246hIesr496yLD+eOHeJSdBtxxKgB+Gmk4ydsrR1trDIlK0Yq3l9J2XVPTT+/pKtyKmRxLWwNGHvhdJDfs92MiS3+7M=", + "version": "3.7.9"}, "job_id": 4342593698, "msgs": [{"INFO": "login: Login successful", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 14:45:37 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: '{"rdata":{"exchange":"mx.example.com.","preference":123},"ttl":"12345"}' + form: {} + headers: + Auth-Token: + - 5Trj0G1M2B0g1t1IY09yFwGpn31tjWNRNU81RhYHaUp6kxGa3UVK5F9hQqlZBu9SNLxkj6cAk6q93ndW246hIesr496yLD+eOHeJSdBtxxKgB+Gmk4ydsrR1trDIlK0Yq3l9J2XVPTT+/pKtyKmRxLWwNGHvhdJDfs92MiS3+7M= + Content-Type: + - application/json + url: https://api.dynect.net/REST/MXRecord/go-dynect.test/go-dynect.test + method: POST + response: + body: '{"status": "success", "data": {"zone": "go-dynect.test", "ttl": 12345, + "fqdn": "go-dynect.test", "record_type": "MX", "rdata": {"preference": 123, + "exchange": "mx.example.com."}, "record_id": 0}, "job_id": 4342593703, "msgs": + [{"INFO": "add: Record added", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 14:45:37 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: '{"publish":true}' + form: {} + headers: + Auth-Token: + - 5Trj0G1M2B0g1t1IY09yFwGpn31tjWNRNU81RhYHaUp6kxGa3UVK5F9hQqlZBu9SNLxkj6cAk6q93ndW246hIesr496yLD+eOHeJSdBtxxKgB+Gmk4ydsrR1trDIlK0Yq3l9J2XVPTT+/pKtyKmRxLWwNGHvhdJDfs92MiS3+7M= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Zone/go-dynect.test + method: PUT + response: + body: '{"status": "success", "data": {"zone_type": "Primary", "task_id": "230305365", + "serial": 2017122005, "serial_style": "day", "zone": "go-dynect.test"}, "job_id": + 4342593710, "msgs": [{"INFO": "publish: go-dynect.test published", "SOURCE": + "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 14:45:37 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - 5Trj0G1M2B0g1t1IY09yFwGpn31tjWNRNU81RhYHaUp6kxGa3UVK5F9hQqlZBu9SNLxkj6cAk6q93ndW246hIesr496yLD+eOHeJSdBtxxKgB+Gmk4ydsrR1trDIlK0Yq3l9J2XVPTT+/pKtyKmRxLWwNGHvhdJDfs92MiS3+7M= + Content-Type: + - application/json + url: https://api.dynect.net/REST/AllRecord/go-dynect.test/go-dynect.test + method: GET + response: + body: '{"status": "success", "data": ["/REST/CNAMERecord/go-dynect.test/foo.go-dynect.test/318905322", + "/REST/SOARecord/go-dynect.test/go-dynect.test/318812133", "/REST/MXRecord/go-dynect.test/go-dynect.test/319018246", + "/REST/NSRecord/go-dynect.test/go-dynect.test/318812135", "/REST/NSRecord/go-dynect.test/go-dynect.test/318812136", + "/REST/NSRecord/go-dynect.test/go-dynect.test/318812137", "/REST/NSRecord/go-dynect.test/go-dynect.test/318812138", + "/REST/ARecord/go-dynect.test/foobar.go-dynect.test/319014258"], "job_id": 4342593722, + "msgs": [{"INFO": "get_tree: Here is your zone tree", "SOURCE": "BLL", "ERR_CD": + null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 14:45:37 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - 5Trj0G1M2B0g1t1IY09yFwGpn31tjWNRNU81RhYHaUp6kxGa3UVK5F9hQqlZBu9SNLxkj6cAk6q93ndW246hIesr496yLD+eOHeJSdBtxxKgB+Gmk4ydsrR1trDIlK0Yq3l9J2XVPTT+/pKtyKmRxLWwNGHvhdJDfs92MiS3+7M= + Content-Type: + - application/json + url: https://api.dynect.net/REST/MXRecord/go-dynect.test/go-dynect.test/319018246 + method: GET + response: + body: '{"status": "success", "data": {"zone": "go-dynect.test", "ttl": 12345, + "fqdn": "go-dynect.test", "record_type": "MX", "rdata": {"preference": 123, + "exchange": "mx.example.com."}, "record_id": 319018246}, "job_id": 4342593727, + "msgs": [{"INFO": "get: Found the record", "SOURCE": "API-B", "ERR_CD": null, + "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 14:45:37 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - 5Trj0G1M2B0g1t1IY09yFwGpn31tjWNRNU81RhYHaUp6kxGa3UVK5F9hQqlZBu9SNLxkj6cAk6q93ndW246hIesr496yLD+eOHeJSdBtxxKgB+Gmk4ydsrR1trDIlK0Yq3l9J2XVPTT+/pKtyKmRxLWwNGHvhdJDfs92MiS3+7M= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: DELETE + response: + body: '{"status": "success", "data": {}, "job_id": 4342593732, "msgs": [{"INFO": + "logout: Logout successful", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 14:45:37 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 diff --git a/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_create_zone.yaml b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_create_zone.yaml new file mode 100644 index 0000000000..af6ce94ce0 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_create_zone.yaml @@ -0,0 +1,134 @@ +--- +version: 1 +rwmutex: {} +interactions: +- request: + body: '{"user_name":"dynect-user","password":"p@55w0rd","customer_name":"go-dynect"}' + form: {} + headers: + Auth-Token: + - "" + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: POST + response: + body: '{"status": "success", "data": {"token": "vTjIj83plGwG5lf+2gzYpI/JlEh8JQSM88ang3OVjUQfe8JfirVWlvl1786+nJbsg987HR6aiIcx6MuseIOvvNaeqxFwwR9xTJvP5EikWS2Cn/Xg/WVDulJ66xl1vjxusJVVlhgLY0MF0nGlBbVMjIYILxbg9pbkB7N4WL+dgu4=", + "version": "3.7.9"}, "job_id": 4344013458, "msgs": [{"INFO": "login: Login successful", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Thu, 21 Dec 2017 00:57:07 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: '{"rname":"admin@example.com","serial_style":"day","ttl":"1800"}' + form: {} + headers: + Auth-Token: + - vTjIj83plGwG5lf+2gzYpI/JlEh8JQSM88ang3OVjUQfe8JfirVWlvl1786+nJbsg987HR6aiIcx6MuseIOvvNaeqxFwwR9xTJvP5EikWS2Cn/Xg/WVDulJ66xl1vjxusJVVlhgLY0MF0nGlBbVMjIYILxbg9pbkB7N4WL+dgu4= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Zone/subzone.go-dynect.test/ + method: POST + response: + body: '{"status": "success", "data": {"zone_type": "Primary", "serial_style": + "day", "serial": 0, "zone": "subzone.go-dynect.test"}, "job_id": 4344013466, + "msgs": [{"INFO": "setup: If you plan to provide your own secondary DNS for + the zone, allow notify requests from these IP addresses on your nameserver: + 208.78.68.66, 2600:2003:0:1::66", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}, + {"INFO": "create: New zone subzone.go-dynect.test created. Publish it to put + it on our server.", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Thu, 21 Dec 2017 00:57:09 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: '{"publish":true}' + form: {} + headers: + Auth-Token: + - vTjIj83plGwG5lf+2gzYpI/JlEh8JQSM88ang3OVjUQfe8JfirVWlvl1786+nJbsg987HR6aiIcx6MuseIOvvNaeqxFwwR9xTJvP5EikWS2Cn/Xg/WVDulJ66xl1vjxusJVVlhgLY0MF0nGlBbVMjIYILxbg9pbkB7N4WL+dgu4= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Zone/subzone.go-dynect.test + method: PUT + response: + body: '{"status": "success", "data": {"zone_type": "Primary", "task_id": "230378639", + "serial": 2017122100, "serial_style": "day", "zone": "subzone.go-dynect.test"}, + "job_id": 4344013515, "msgs": [{"INFO": "publish: subzone.go-dynect.test published", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Thu, 21 Dec 2017 00:57:09 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - vTjIj83plGwG5lf+2gzYpI/JlEh8JQSM88ang3OVjUQfe8JfirVWlvl1786+nJbsg987HR6aiIcx6MuseIOvvNaeqxFwwR9xTJvP5EikWS2Cn/Xg/WVDulJ66xl1vjxusJVVlhgLY0MF0nGlBbVMjIYILxbg9pbkB7N4WL+dgu4= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Zone/subzone.go-dynect.test + method: GET + response: + body: '{"status": "success", "data": {"zone_type": "Primary", "serial_style": + "day", "serial": 2017122100, "zone": "subzone.go-dynect.test"}, "job_id": 4344013521, + "msgs": [{"INFO": "get: Your zone, subzone.go-dynect.test", "SOURCE": "BLL", + "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Thu, 21 Dec 2017 00:57:09 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - vTjIj83plGwG5lf+2gzYpI/JlEh8JQSM88ang3OVjUQfe8JfirVWlvl1786+nJbsg987HR6aiIcx6MuseIOvvNaeqxFwwR9xTJvP5EikWS2Cn/Xg/WVDulJ66xl1vjxusJVVlhgLY0MF0nGlBbVMjIYILxbg9pbkB7N4WL+dgu4= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: DELETE + response: + body: '{"status": "success", "data": {}, "job_id": 4344013527, "msgs": [{"INFO": + "logout: Logout successful", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Thu, 21 Dec 2017 00:57:09 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 diff --git a/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_delete_sub_zone.yaml b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_delete_sub_zone.yaml new file mode 100644 index 0000000000..9687b72cba --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_delete_sub_zone.yaml @@ -0,0 +1,153 @@ +--- +version: 1 +rwmutex: {} +interactions: +- request: + body: '{"user_name":"dynect-user","password":"p@55w0rd","customer_name":"go-dynect"}' + form: {} + headers: + Auth-Token: + - "" + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: POST + response: + body: '{"status": "success", "data": {"token": "THqiI1SkDZateC3XF1y3bF6ZlPlYlIP+uEAr7mE9G3XT84cwTLNkbmArR4xGPnfJmsKYOT+mN3PO0z1G8wu4R7W4DufXuywZoNQWYxv51+X3ZQd0MkFA7OMvtPTRxts+E0Kc5HGLjqmZLD/AEpfHu/5XemknyRMD", + "version": "3.7.9"}, "job_id": 4349697720, "msgs": [{"INFO": "login: Login successful", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 22 Dec 2017 17:43:51 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - THqiI1SkDZateC3XF1y3bF6ZlPlYlIP+uEAr7mE9G3XT84cwTLNkbmArR4xGPnfJmsKYOT+mN3PO0z1G8wu4R7W4DufXuywZoNQWYxv51+X3ZQd0MkFA7OMvtPTRxts+E0Kc5HGLjqmZLD/AEpfHu/5XemknyRMD + Content-Type: + - application/json + url: https://api.dynect.net/REST/Zone/subzone.go-dynect.test/ + method: DELETE + response: + body: '{"status": "success", "data": {}, "job_id": 4349697722, "msgs": [{"INFO": + "remove: Zone removed", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 22 Dec 2017 17:43:51 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - THqiI1SkDZateC3XF1y3bF6ZlPlYlIP+uEAr7mE9G3XT84cwTLNkbmArR4xGPnfJmsKYOT+mN3PO0z1G8wu4R7W4DufXuywZoNQWYxv51+X3ZQd0MkFA7OMvtPTRxts+E0Kc5HGLjqmZLD/AEpfHu/5XemknyRMD + Content-Type: + - application/json + url: https://api.dynect.net/REST/Node/go-dynect.test/subzone.go-dynect.test + method: DELETE + response: + body: '{"status": "success", "data": {"zone_type": "Primary", "serial_style": + "day", "serial": 2017122201, "zone": "go-dynect.test"}, "job_id": 4349697733, + "msgs": [{"INFO": "remove_node: subzone.go-dynect.test removed from tree. All + records also removed.", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 22 Dec 2017 17:43:51 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: '{"publish":true}' + form: {} + headers: + Auth-Token: + - THqiI1SkDZateC3XF1y3bF6ZlPlYlIP+uEAr7mE9G3XT84cwTLNkbmArR4xGPnfJmsKYOT+mN3PO0z1G8wu4R7W4DufXuywZoNQWYxv51+X3ZQd0MkFA7OMvtPTRxts+E0Kc5HGLjqmZLD/AEpfHu/5XemknyRMD + Content-Type: + - application/json + url: https://api.dynect.net/REST/Zone/go-dynect.test + method: PUT + response: + body: '{"status": "success", "data": {"zone_type": "Primary", "task_id": "230665571", + "serial": 2017122202, "serial_style": "day", "zone": "go-dynect.test"}, "job_id": + 4349697739, "msgs": [{"INFO": "publish: go-dynect.test published", "SOURCE": + "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 22 Dec 2017 17:43:52 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - THqiI1SkDZateC3XF1y3bF6ZlPlYlIP+uEAr7mE9G3XT84cwTLNkbmArR4xGPnfJmsKYOT+mN3PO0z1G8wu4R7W4DufXuywZoNQWYxv51+X3ZQd0MkFA7OMvtPTRxts+E0Kc5HGLjqmZLD/AEpfHu/5XemknyRMD + Content-Type: + - application/json + url: https://api.dynect.net/REST/Zone/subzone.go-dynect.test + method: GET + response: + body: '{"status": "failure", "data": {}, "job_id": 4349697747, "msgs": [{"INFO": + "zone: No such zone", "SOURCE": "API-B", "ERR_CD": "NOT_FOUND", "LVL": "ERROR"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 22 Dec 2017 17:43:52 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 404 Not Found + code: 404 +- request: + body: "" + form: {} + headers: + Auth-Token: + - THqiI1SkDZateC3XF1y3bF6ZlPlYlIP+uEAr7mE9G3XT84cwTLNkbmArR4xGPnfJmsKYOT+mN3PO0z1G8wu4R7W4DufXuywZoNQWYxv51+X3ZQd0MkFA7OMvtPTRxts+E0Kc5HGLjqmZLD/AEpfHu/5XemknyRMD + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: DELETE + response: + body: '{"status": "success", "data": {}, "job_id": 4349697755, "msgs": [{"INFO": + "logout: Logout successful", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 22 Dec 2017 17:43:52 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 diff --git a/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_delete_zone.yaml b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_delete_zone.yaml new file mode 100644 index 0000000000..3dc96063c2 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_delete_zone.yaml @@ -0,0 +1,101 @@ +--- +version: 1 +rwmutex: {} +interactions: +- request: + body: '{"user_name":"dynect-user","password":"p@55w0rd","customer_name":"go-dynect"}' + form: {} + headers: + Auth-Token: + - "" + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: POST + response: + body: '{"status": "success", "data": {"token": "ehqnDu44eQcNVPhHf+iyRd8/Ilgx5SpF0uR7OCPbjNGMA131GlouJtqLN5VS8flT+cChPirW9NEbjo3PfJOJmCasbumDfEg7PdAyd2rOvhKG4/XHze/FRv7bAnsFFafZHL5wfSoGgqdZlv+vZRJctWpDkXLHt9RHM8UejSS0/Qo=", + "version": "3.7.9"}, "job_id": 4349699356, "msgs": [{"INFO": "login: Login successful", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 22 Dec 2017 17:44:40 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - ehqnDu44eQcNVPhHf+iyRd8/Ilgx5SpF0uR7OCPbjNGMA131GlouJtqLN5VS8flT+cChPirW9NEbjo3PfJOJmCasbumDfEg7PdAyd2rOvhKG4/XHze/FRv7bAnsFFafZHL5wfSoGgqdZlv+vZRJctWpDkXLHt9RHM8UejSS0/Qo= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Zone/zone-go-dynect.test/ + method: DELETE + response: + body: '{"status": "success", "data": {}, "job_id": 4349699362, "msgs": [{"INFO": + "remove: Zone removed", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 22 Dec 2017 17:44:41 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - ehqnDu44eQcNVPhHf+iyRd8/Ilgx5SpF0uR7OCPbjNGMA131GlouJtqLN5VS8flT+cChPirW9NEbjo3PfJOJmCasbumDfEg7PdAyd2rOvhKG4/XHze/FRv7bAnsFFafZHL5wfSoGgqdZlv+vZRJctWpDkXLHt9RHM8UejSS0/Qo= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Zone/zone-go-dynect.test + method: GET + response: + body: '{"status": "failure", "data": {}, "job_id": 4349699368, "msgs": [{"INFO": + "zone: No such zone", "SOURCE": "API-B", "ERR_CD": "NOT_FOUND", "LVL": "ERROR"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 22 Dec 2017 17:44:41 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 404 Not Found + code: 404 +- request: + body: "" + form: {} + headers: + Auth-Token: + - ehqnDu44eQcNVPhHf+iyRd8/Ilgx5SpF0uR7OCPbjNGMA131GlouJtqLN5VS8flT+cChPirW9NEbjo3PfJOJmCasbumDfEg7PdAyd2rOvhKG4/XHze/FRv7bAnsFFafZHL5wfSoGgqdZlv+vZRJctWpDkXLHt9RHM8UejSS0/Qo= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: DELETE + response: + body: '{"status": "success", "data": {}, "job_id": 4349699370, "msgs": [{"INFO": + "logout: Logout successful", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 22 Dec 2017 17:44:41 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 diff --git a/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_get_a.yaml b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_get_a.yaml new file mode 100644 index 0000000000..1a1a0ba312 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_get_a.yaml @@ -0,0 +1,104 @@ +--- +version: 1 +rwmutex: {} +interactions: +- request: + body: '{"user_name":"dynect-user","password":"p@55w0rd","customer_name":"go-dynect"}' + form: {} + headers: + Auth-Token: + - "" + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: POST + response: + body: '{"status": "success", "data": {"token": "S7uWFq5OnRrL0divNfQgijM1gPTh8aIa3qHvoH+t1GVF84hwfMF8e9BD5ty09DuMprfDW1pDMgG45mEYNJE+KT2Xow8s5tfcA9mijaNemE+7gQ4DlkVd7PHggpUckUFN+faA5vPOTfSEn6T+MEux5ZoTnncnawkgqtu40DhzVV8=", + "version": "3.7.9"}, "job_id": 4342164998, "msgs": [{"INFO": "login: Login successful", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 11:22:46 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - S7uWFq5OnRrL0divNfQgijM1gPTh8aIa3qHvoH+t1GVF84hwfMF8e9BD5ty09DuMprfDW1pDMgG45mEYNJE+KT2Xow8s5tfcA9mijaNemE+7gQ4DlkVd7PHggpUckUFN+faA5vPOTfSEn6T+MEux5ZoTnncnawkgqtu40DhzVV8= + Content-Type: + - application/json + url: https://api.dynect.net/REST/AllRecord/go-dynect.test/foobar.go-dynect.test + method: GET + response: + body: '{"status": "success", "data": ["/REST/ARecord/go-dynect.test/foobar.go-dynect.test/318905321"], + "job_id": 4342165004, "msgs": [{"INFO": "get_tree: Here is your zone tree", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 11:22:46 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - S7uWFq5OnRrL0divNfQgijM1gPTh8aIa3qHvoH+t1GVF84hwfMF8e9BD5ty09DuMprfDW1pDMgG45mEYNJE+KT2Xow8s5tfcA9mijaNemE+7gQ4DlkVd7PHggpUckUFN+faA5vPOTfSEn6T+MEux5ZoTnncnawkgqtu40DhzVV8= + Content-Type: + - application/json + url: https://api.dynect.net/REST/ARecord/go-dynect.test/foobar.go-dynect.test/318905321 + method: GET + response: + body: '{"status": "success", "data": {"zone": "go-dynect.test", "ttl": 3600, "fqdn": + "foobar.go-dynect.test", "record_type": "A", "rdata": {"address": "10.9.8.7"}, + "record_id": 318905321}, "job_id": 4342165009, "msgs": [{"INFO": "get: Found + the record", "SOURCE": "API-B", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 11:22:46 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - S7uWFq5OnRrL0divNfQgijM1gPTh8aIa3qHvoH+t1GVF84hwfMF8e9BD5ty09DuMprfDW1pDMgG45mEYNJE+KT2Xow8s5tfcA9mijaNemE+7gQ4DlkVd7PHggpUckUFN+faA5vPOTfSEn6T+MEux5ZoTnncnawkgqtu40DhzVV8= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: DELETE + response: + body: '{"status": "success", "data": {}, "job_id": 4342165013, "msgs": [{"INFO": + "logout: Logout successful", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 11:22:47 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 diff --git a/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_get_a_not_found.yaml b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_get_a_not_found.yaml new file mode 100644 index 0000000000..a74a6b5757 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_get_a_not_found.yaml @@ -0,0 +1,79 @@ +--- +version: 1 +rwmutex: {} +interactions: +- request: + body: '{"user_name":"dynect-user","password":"p@55w0rd","customer_name":"go-dynect"}' + form: {} + headers: + Auth-Token: + - "" + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: POST + response: + body: '{"status": "success", "data": {"token": "l5QZksz8FRhr+DekJ0SHgoeziztmUOyFYVtVhjS/yOLQSqS/fr72nuCtQhtQtUoJLperzxQ6wid9CIg6i5SOlzBBv2iVeYUAr4ilU1jueUcuS/AYNoRU6O6IBegImDB+nP1+Ao7MekShnUZfUr2e3spRYIUg3eUg60hBa61nT70=", + "version": "3.7.9"}, "job_id": 4342199438, "msgs": [{"INFO": "login: Login successful", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 11:38:50 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - l5QZksz8FRhr+DekJ0SHgoeziztmUOyFYVtVhjS/yOLQSqS/fr72nuCtQhtQtUoJLperzxQ6wid9CIg6i5SOlzBBv2iVeYUAr4ilU1jueUcuS/AYNoRU6O6IBegImDB+nP1+Ao7MekShnUZfUr2e3spRYIUg3eUg60hBa61nT70= + Content-Type: + - application/json + url: https://api.dynect.net/REST/AllRecord/go-dynect.test/unknown.go-dynect.test + method: GET + response: + body: '{"status": "failure", "data": {}, "job_id": 4342199445, "msgs": [{"INFO": + "node: Node is not in the zone", "SOURCE": "BLL", "ERR_CD": "NOT_FOUND", "LVL": + "ERROR"}, {"INFO": "get_tree: Node name not found within the zone", "SOURCE": + "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 11:38:50 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 404 Not Found + code: 404 +- request: + body: "" + form: {} + headers: + Auth-Token: + - l5QZksz8FRhr+DekJ0SHgoeziztmUOyFYVtVhjS/yOLQSqS/fr72nuCtQhtQtUoJLperzxQ6wid9CIg6i5SOlzBBv2iVeYUAr4ilU1jueUcuS/AYNoRU6O6IBegImDB+nP1+Ao7MekShnUZfUr2e3spRYIUg3eUg60hBa61nT70= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: DELETE + response: + body: '{"status": "success", "data": {}, "job_id": 4342199451, "msgs": [{"INFO": + "logout: Logout successful", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 11:38:50 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 diff --git a/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_get_cname.yaml b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_get_cname.yaml new file mode 100644 index 0000000000..a1eea9b582 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/fixtures/convenient_get_cname.yaml @@ -0,0 +1,104 @@ +--- +version: 1 +rwmutex: {} +interactions: +- request: + body: '{"user_name":"dynect-user","password":"p@55w0rd","customer_name":"go-dynect"}' + form: {} + headers: + Auth-Token: + - "" + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: POST + response: + body: '{"status": "success", "data": {"token": "JhswjAiu7O3gRKuQlCTCuurkwkdZZ0yYOzTZiUa9Fkr5YlzPmQIQJGtqOKV4dGmaYIkldRpIDbH6muKPDSmoa6TMFv0SNH0+vj6MgGeOqmW2H6vahp6ENWlZICR5ra56OTANL4CNuznc8PAp1e6dQI4yAsfZ9J1ZFKjrajm67K4=", + "version": "3.7.9"}, "job_id": 4342161224, "msgs": [{"INFO": "login: Login successful", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 11:20:42 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - JhswjAiu7O3gRKuQlCTCuurkwkdZZ0yYOzTZiUa9Fkr5YlzPmQIQJGtqOKV4dGmaYIkldRpIDbH6muKPDSmoa6TMFv0SNH0+vj6MgGeOqmW2H6vahp6ENWlZICR5ra56OTANL4CNuznc8PAp1e6dQI4yAsfZ9J1ZFKjrajm67K4= + Content-Type: + - application/json + url: https://api.dynect.net/REST/AllRecord/go-dynect.test/foo.go-dynect.test + method: GET + response: + body: '{"status": "success", "data": ["/REST/CNAMERecord/go-dynect.test/foo.go-dynect.test/318905322"], + "job_id": 4342161229, "msgs": [{"INFO": "get_tree: Here is your zone tree", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 11:20:42 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - JhswjAiu7O3gRKuQlCTCuurkwkdZZ0yYOzTZiUa9Fkr5YlzPmQIQJGtqOKV4dGmaYIkldRpIDbH6muKPDSmoa6TMFv0SNH0+vj6MgGeOqmW2H6vahp6ENWlZICR5ra56OTANL4CNuznc8PAp1e6dQI4yAsfZ9J1ZFKjrajm67K4= + Content-Type: + - application/json + url: https://api.dynect.net/REST/CNAMERecord/go-dynect.test/foo.go-dynect.test/318905322 + method: GET + response: + body: '{"status": "success", "data": {"zone": "go-dynect.test", "ttl": 3600, "fqdn": + "foo.go-dynect.test", "record_type": "CNAME", "rdata": {"cname": "foobar.go-dynect.test."}, + "record_id": 318905322}, "job_id": 4342161235, "msgs": [{"INFO": "get: Found + the record", "SOURCE": "API-B", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 11:20:42 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - JhswjAiu7O3gRKuQlCTCuurkwkdZZ0yYOzTZiUa9Fkr5YlzPmQIQJGtqOKV4dGmaYIkldRpIDbH6muKPDSmoa6TMFv0SNH0+vj6MgGeOqmW2H6vahp6ENWlZICR5ra56OTANL4CNuznc8PAp1e6dQI4yAsfZ9J1ZFKjrajm67K4= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: DELETE + response: + body: '{"status": "success", "data": {}, "job_id": 4342161240, "msgs": [{"INFO": + "logout: Logout successful", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 11:20:42 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 diff --git a/vendor/github.com/nesv/go-dynect/dynect/fixtures/fetching_all_zone_records.yaml b/vendor/github.com/nesv/go-dynect/dynect/fixtures/fetching_all_zone_records.yaml new file mode 100644 index 0000000000..c2e8507dfd --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/fixtures/fetching_all_zone_records.yaml @@ -0,0 +1,269 @@ +--- +version: 1 +rwmutex: {} +interactions: +- request: + body: '{"user_name":"dynect-user","password":"p@55w0rd","customer_name":"go-dynect"}' + form: {} + headers: + Auth-Token: + - "" + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: POST + response: + body: '{"status": "success", "data": {"token": "/4dfk/aAs9zK0QqVlmx6VHP+3vAvsevOQ6cGM3+KIO9MFiMAEbc52KBi/ayBwkqGP/N1Ou1Kbf7calZZ4tSvIrmH/7EtTox9qGFbgQkvMLXQz7E4GEZSLd7ejFKjxkZh8ttUSBwzkhQZoBPyy0nry1i/jakCgu09P3eAxPiBJ0U=", + "version": "3.7.9"}, "job_id": 4341026959, "msgs": [{"INFO": "login: Login successful", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 03:08:58 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - /4dfk/aAs9zK0QqVlmx6VHP+3vAvsevOQ6cGM3+KIO9MFiMAEbc52KBi/ayBwkqGP/N1Ou1Kbf7calZZ4tSvIrmH/7EtTox9qGFbgQkvMLXQz7E4GEZSLd7ejFKjxkZh8ttUSBwzkhQZoBPyy0nry1i/jakCgu09P3eAxPiBJ0U= + Content-Type: + - application/json + url: https://api.dynect.net/REST/AllRecord/go-dynect.test + method: GET + response: + body: '{"status": "success", "data": ["/REST/CNAMERecord/go-dynect.test/foo.go-dynect.test/318905322", + "/REST/SOARecord/go-dynect.test/go-dynect.test/318812133", "/REST/NSRecord/go-dynect.test/go-dynect.test/318812135", + "/REST/NSRecord/go-dynect.test/go-dynect.test/318812136", "/REST/NSRecord/go-dynect.test/go-dynect.test/318812137", + "/REST/NSRecord/go-dynect.test/go-dynect.test/318812138", "/REST/ARecord/go-dynect.test/foobar.go-dynect.test/318905321"], + "job_id": 4341026968, "msgs": [{"INFO": "get_tree: Here is your zone tree", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 03:08:58 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - /4dfk/aAs9zK0QqVlmx6VHP+3vAvsevOQ6cGM3+KIO9MFiMAEbc52KBi/ayBwkqGP/N1Ou1Kbf7calZZ4tSvIrmH/7EtTox9qGFbgQkvMLXQz7E4GEZSLd7ejFKjxkZh8ttUSBwzkhQZoBPyy0nry1i/jakCgu09P3eAxPiBJ0U= + Content-Type: + - application/json + url: https://api.dynect.net/REST/CNAMERecord/go-dynect.test/foo.go-dynect.test/318905322 + method: GET + response: + body: '{"status": "success", "data": {"zone": "go-dynect.test", "ttl": 3600, "fqdn": + "foo.go-dynect.test", "record_type": "CNAME", "rdata": {"cname": "foobar.go-dynect.test."}, + "record_id": 318905322}, "job_id": 4341026976, "msgs": [{"INFO": "get: Found + the record", "SOURCE": "API-B", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 03:08:58 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - /4dfk/aAs9zK0QqVlmx6VHP+3vAvsevOQ6cGM3+KIO9MFiMAEbc52KBi/ayBwkqGP/N1Ou1Kbf7calZZ4tSvIrmH/7EtTox9qGFbgQkvMLXQz7E4GEZSLd7ejFKjxkZh8ttUSBwzkhQZoBPyy0nry1i/jakCgu09P3eAxPiBJ0U= + Content-Type: + - application/json + url: https://api.dynect.net/REST/SOARecord/go-dynect.test/go-dynect.test/318812133 + method: GET + response: + body: '{"status": "success", "data": {"zone": "go-dynect.test", "ttl": 3600, "fqdn": + "go-dynect.test", "record_type": "SOA", "rdata": {"rname": "admin@go-dynect.com.", + "retry": 600, "mname": "ns1.p19.dynect.net.", "minimum": 1800, "refresh": 3600, + "expire": 604800, "serial": 2017122000}, "record_id": 318812133, "serial_style": + "day"}, "job_id": 4341026980, "msgs": [{"INFO": "get: Found the record", "SOURCE": + "API-B", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 03:08:58 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - /4dfk/aAs9zK0QqVlmx6VHP+3vAvsevOQ6cGM3+KIO9MFiMAEbc52KBi/ayBwkqGP/N1Ou1Kbf7calZZ4tSvIrmH/7EtTox9qGFbgQkvMLXQz7E4GEZSLd7ejFKjxkZh8ttUSBwzkhQZoBPyy0nry1i/jakCgu09P3eAxPiBJ0U= + Content-Type: + - application/json + url: https://api.dynect.net/REST/NSRecord/go-dynect.test/go-dynect.test/318812135 + method: GET + response: + body: '{"status": "success", "data": {"zone": "go-dynect.test", "service_class": + "Primary", "ttl": 86400, "fqdn": "go-dynect.test", "record_type": "NS", "rdata": + {"nsdname": "ns1.p19.dynect.net."}, "record_id": 318812135}, "job_id": 4341026990, + "msgs": [{"INFO": "get: Found the record", "SOURCE": "API-B", "ERR_CD": null, + "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 03:08:59 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - /4dfk/aAs9zK0QqVlmx6VHP+3vAvsevOQ6cGM3+KIO9MFiMAEbc52KBi/ayBwkqGP/N1Ou1Kbf7calZZ4tSvIrmH/7EtTox9qGFbgQkvMLXQz7E4GEZSLd7ejFKjxkZh8ttUSBwzkhQZoBPyy0nry1i/jakCgu09P3eAxPiBJ0U= + Content-Type: + - application/json + url: https://api.dynect.net/REST/NSRecord/go-dynect.test/go-dynect.test/318812136 + method: GET + response: + body: '{"status": "success", "data": {"zone": "go-dynect.test", "service_class": + "Primary", "ttl": 86400, "fqdn": "go-dynect.test", "record_type": "NS", "rdata": + {"nsdname": "ns2.p19.dynect.net."}, "record_id": 318812136}, "job_id": 4341026995, + "msgs": [{"INFO": "get: Found the record", "SOURCE": "API-B", "ERR_CD": null, + "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 03:08:59 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - /4dfk/aAs9zK0QqVlmx6VHP+3vAvsevOQ6cGM3+KIO9MFiMAEbc52KBi/ayBwkqGP/N1Ou1Kbf7calZZ4tSvIrmH/7EtTox9qGFbgQkvMLXQz7E4GEZSLd7ejFKjxkZh8ttUSBwzkhQZoBPyy0nry1i/jakCgu09P3eAxPiBJ0U= + Content-Type: + - application/json + url: https://api.dynect.net/REST/NSRecord/go-dynect.test/go-dynect.test/318812137 + method: GET + response: + body: '{"status": "success", "data": {"zone": "go-dynect.test", "service_class": + "Primary", "ttl": 86400, "fqdn": "go-dynect.test", "record_type": "NS", "rdata": + {"nsdname": "ns3.p19.dynect.net."}, "record_id": 318812137}, "job_id": 4341027001, + "msgs": [{"INFO": "get: Found the record", "SOURCE": "API-B", "ERR_CD": null, + "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 03:08:59 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - /4dfk/aAs9zK0QqVlmx6VHP+3vAvsevOQ6cGM3+KIO9MFiMAEbc52KBi/ayBwkqGP/N1Ou1Kbf7calZZ4tSvIrmH/7EtTox9qGFbgQkvMLXQz7E4GEZSLd7ejFKjxkZh8ttUSBwzkhQZoBPyy0nry1i/jakCgu09P3eAxPiBJ0U= + Content-Type: + - application/json + url: https://api.dynect.net/REST/NSRecord/go-dynect.test/go-dynect.test/318812138 + method: GET + response: + body: '{"status": "success", "data": {"zone": "go-dynect.test", "service_class": + "Primary", "ttl": 86400, "fqdn": "go-dynect.test", "record_type": "NS", "rdata": + {"nsdname": "ns4.p19.dynect.net."}, "record_id": 318812138}, "job_id": 4341027006, + "msgs": [{"INFO": "get: Found the record", "SOURCE": "API-B", "ERR_CD": null, + "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 03:08:59 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - /4dfk/aAs9zK0QqVlmx6VHP+3vAvsevOQ6cGM3+KIO9MFiMAEbc52KBi/ayBwkqGP/N1Ou1Kbf7calZZ4tSvIrmH/7EtTox9qGFbgQkvMLXQz7E4GEZSLd7ejFKjxkZh8ttUSBwzkhQZoBPyy0nry1i/jakCgu09P3eAxPiBJ0U= + Content-Type: + - application/json + url: https://api.dynect.net/REST/ARecord/go-dynect.test/foobar.go-dynect.test/318905321 + method: GET + response: + body: '{"status": "success", "data": {"zone": "go-dynect.test", "ttl": 3600, "fqdn": + "foobar.go-dynect.test", "record_type": "A", "rdata": {"address": "10.9.8.7"}, + "record_id": 318905321}, "job_id": 4341027017, "msgs": [{"INFO": "get: Found + the record", "SOURCE": "API-B", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 03:08:59 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - /4dfk/aAs9zK0QqVlmx6VHP+3vAvsevOQ6cGM3+KIO9MFiMAEbc52KBi/ayBwkqGP/N1Ou1Kbf7calZZ4tSvIrmH/7EtTox9qGFbgQkvMLXQz7E4GEZSLd7ejFKjxkZh8ttUSBwzkhQZoBPyy0nry1i/jakCgu09P3eAxPiBJ0U= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: DELETE + response: + body: '{"status": "success", "data": {}, "job_id": 4341027020, "msgs": [{"INFO": + "logout: Logout successful", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 03:09:00 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 diff --git a/vendor/github.com/nesv/go-dynect/dynect/fixtures/login_logout.yaml b/vendor/github.com/nesv/go-dynect/dynect/fixtures/login_logout.yaml new file mode 100644 index 0000000000..cd13281bfb --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/fixtures/login_logout.yaml @@ -0,0 +1,53 @@ +--- +version: 1 +rwmutex: {} +interactions: +- request: + body: '{"user_name":"dynect-user","password":"p@55w0rd","customer_name":"go-dynect"}' + form: {} + headers: + Auth-Token: + - "" + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: POST + response: + body: '{"status": "success", "data": {"token": "+cG++GbemK1hoYxrvq2SFGz00mY78zRZhWntTbLxYF42k22o7w/d0Vk+sEvJayq5jSo8ivphahLFmZV8b99TJMRNZFcpFC0NyYeyL/7l8Grsdpplh6l1pMmkInSe3mXVuvgKS5cVSUN5Z8e6DVf9K0Jz/aLZBeL70Qc5VQktaKc=", + "version": "3.7.9"}, "job_id": 4340980452, "msgs": [{"INFO": "login: Login successful", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 02:46:34 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - +cG++GbemK1hoYxrvq2SFGz00mY78zRZhWntTbLxYF42k22o7w/d0Vk+sEvJayq5jSo8ivphahLFmZV8b99TJMRNZFcpFC0NyYeyL/7l8Grsdpplh6l1pMmkInSe3mXVuvgKS5cVSUN5Z8e6DVf9K0Jz/aLZBeL70Qc5VQktaKc= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: DELETE + response: + body: '{"status": "success", "data": {}, "job_id": 4340980466, "msgs": [{"INFO": + "logout: Logout successful", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 02:46:34 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 diff --git a/vendor/github.com/nesv/go-dynect/dynect/fixtures/zones_request.yaml b/vendor/github.com/nesv/go-dynect/dynect/fixtures/zones_request.yaml new file mode 100644 index 0000000000..d7a461d492 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/fixtures/zones_request.yaml @@ -0,0 +1,79 @@ +--- +version: 1 +rwmutex: {} +interactions: +- request: + body: '{"user_name":"dynect-user","password":"p@55w0rd","customer_name":"go-dynect"}' + form: {} + headers: + Auth-Token: + - "" + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: POST + response: + body: '{"status": "success", "data": {"token": "PBz+i35+fKgVUdmzmvGJzcq0F+p+ExJI9Y5fe8VgiJZhFFsY/Vp2KVZb9JBj/CSCewT7rum6IgoBxf8BzK2LuTNzFSgzsWkztKsF+awruRWFdAtl8XfBoG6pIDAcLIgjuE/vUt4WOUH007w6G7FTKt+dojSTK19mw130KtUHik8=", + "version": "3.7.9"}, "job_id": 4341009633, "msgs": [{"INFO": "login: Login successful", + "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 03:00:16 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - PBz+i35+fKgVUdmzmvGJzcq0F+p+ExJI9Y5fe8VgiJZhFFsY/Vp2KVZb9JBj/CSCewT7rum6IgoBxf8BzK2LuTNzFSgzsWkztKsF+awruRWFdAtl8XfBoG6pIDAcLIgjuE/vUt4WOUH007w6G7FTKt+dojSTK19mw130KtUHik8= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Zone + method: GET + response: + body: '{"status": "success", "data": ["/REST/Zone/example.com/", + "/REST/Zone/example.net", "/REST/Zone/go-dynect.test/"], "job_id": 4341009645, + "msgs": [{"INFO": "get: Your 3 zones", "SOURCE": "BLL", "ERR_CD": null, + "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 03:00:18 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 +- request: + body: "" + form: {} + headers: + Auth-Token: + - PBz+i35+fKgVUdmzmvGJzcq0F+p+ExJI9Y5fe8VgiJZhFFsY/Vp2KVZb9JBj/CSCewT7rum6IgoBxf8BzK2LuTNzFSgzsWkztKsF+awruRWFdAtl8XfBoG6pIDAcLIgjuE/vUt4WOUH007w6G7FTKt+dojSTK19mw130KtUHik8= + Content-Type: + - application/json + url: https://api.dynect.net/REST/Session + method: DELETE + response: + body: '{"status": "success", "data": {}, "job_id": 4341009725, "msgs": [{"INFO": + "logout: Logout successful", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Wed, 20 Dec 2017 03:00:18 GMT + Server: + - nginx/1.4.6 (Ubuntu) + status: 200 OK + code: 200 diff --git a/vendor/github.com/nesv/go-dynect/dynect/helpers.go b/vendor/github.com/nesv/go-dynect/dynect/helpers.go new file mode 100644 index 0000000000..060dddd596 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/helpers.go @@ -0,0 +1,30 @@ +package dynect + +import "fmt" + +func GetAllDSFServicesDetailed(c *Client) (error, []DSFService) { + var dsfsResponse AllDSFDetailedResponse + requestData := struct { + Detail string `json:"detail"` + }{Detail: "Y"} + + if err := c.Do("GET", "DSF", requestData, &dsfsResponse); err != nil { + return err, nil + } + + return nil, dsfsResponse.Data +} + +func GetDSFServiceDetailed(c *Client, id string) (error, DSFService) { + var dsfsResponse DSFResponse + requestData := struct { + Detail string `json:"detail"` + }{Detail: "Y"} + + loc := fmt.Sprintf("DSF/%s", id) + + if err := c.Do("GET", loc, requestData, &dsfsResponse); err != nil { + return err, DSFService{} + } + return nil, dsfsResponse.Data +} diff --git a/vendor/github.com/nesv/go-dynect/dynect/job.go b/vendor/github.com/nesv/go-dynect/dynect/job.go new file mode 100644 index 0000000000..f502ef2a4c --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/job.go @@ -0,0 +1,8 @@ +package dynect + +type JobData struct { + Status string `json:"status"` + Data interface{} `json:"data"` + ID int `json:"job_id"` + Messages []MessageBlock `json:"msgs"` +} diff --git a/vendor/github.com/nesv/go-dynect/dynect/json.go b/vendor/github.com/nesv/go-dynect/dynect/json.go new file mode 100644 index 0000000000..26f4a037bb --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/json.go @@ -0,0 +1,65 @@ +package dynect + +/* +This struct represents the request body that would be sent to the DynECT API +for logging in and getting a session token for future requests. +*/ +type LoginBlock struct { + Username string `json:"user_name"` + Password string `json:"password"` + CustomerName string `json:"customer_name"` +} + +// Type ResponseBlock holds the "header" information returned by any call to +// the DynECT API. +// +// All response-type structs should include this as an anonymous/embedded field. +type ResponseBlock struct { + Status string `json:"status"` + JobId int `json:"job_id,omitempty"` + Messages []MessageBlock `json:"msgs,omitempty"` +} + +// Type MessageBlock holds the message information from the server, and is +// nested within the ResponseBlock type. +type MessageBlock struct { + Info string `json:"INFO"` + Source string `json:"SOURCE"` + ErrorCode string `json:"ERR_CD"` + Level string `json:"LVL"` +} + +// Type LoginResponse holds the data returned by an HTTP POST call to +// https://api.dynect.net/REST/Session/. +type LoginResponse struct { + ResponseBlock + Data LoginDataBlock `json:"data"` +} + +// Type LoginDataBlock holds the token and API version information from an HTTP +// POST call to https://api.dynect.net/REST/Session/. +// +// It is nested within the LoginResponse struct. +type LoginDataBlock struct { + Token string `json:"token"` + Version string `json:"version"` +} + +// RecordRequest holds the request body for a record create/update +type RecordRequest struct { + RData DataBlock `json:"rdata"` + TTL string `json:"ttl,omitempty"` +} + +// CreateZoneBlock holds the request body for a zone create +type CreateZoneBlock struct { + RName string `json:"rname"` + SerialStyle string `json:"serial_style,omitempty"` + TTL string `json:"ttl"` +} + +// PublishZoneBlock holds the request body for a publish zone request +// https://help.dyn.com/update-zone-api/ +type PublishZoneBlock struct { + Publish bool `json:"publish"` +} diff --git a/vendor/github.com/nesv/go-dynect/dynect/record.go b/vendor/github.com/nesv/go-dynect/dynect/record.go new file mode 100644 index 0000000000..f77f5abde4 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/record.go @@ -0,0 +1,12 @@ +package dynect + +// Record simple struct to hold record details +type Record struct { + ID string + Zone string + Name string + Value string + Type string + FQDN string + TTL string +} diff --git a/vendor/github.com/nesv/go-dynect/dynect/records.go b/vendor/github.com/nesv/go-dynect/dynect/records.go new file mode 100644 index 0000000000..83a65b78ac --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/records.go @@ -0,0 +1,171 @@ +package dynect + +// Type AllRecordsResponse is a struct for holding a list of all URIs returned +// from an HTTP GET call to either https://api.dynect.net/REST/AllRecord/ +// or https://api/dynect.net/REST/AllRecord///. +type AllRecordsResponse struct { + ResponseBlock + Data []string `json:"data"` +} + +// Type RecordResponse is used to hold the information for a single DNS record +// returned from Dyn's DynECT API. +type RecordResponse struct { + ResponseBlock + Data BaseRecord `json:"data"` +} + +/* +The base struct for record data returned from the Dyn REST API. + +It should never be directly passed to the *Client.Do() function for marshaling +response data to. Instead, it should aid in the composition of a more-specific +response struct. +*/ +type BaseRecord struct { + FQDN string `json:"fqdn"` + RecordId int `json:"record_id"` + RecordType string `json:"record_type"` + TTL int `json:"ttl"` + Zone string `json:"zone"` + RData DataBlock `json:"rdata"` +} + +// Type DataBlock is nested within the BaseRecord struct, and is used for +// holding record information. +// +// The comment above each field indicates which record types you can expect +// the information to be provided. +type DataBlock struct { + // A, AAAA + Address string `json:"address,omitempty" bson:"address,omitempty"` + + // ALIAS + Alias string `json:"alias,omitempty" bson:"alias,omitempty"` + + // CERT, DNSKEY, DS, IPSECKEY, KEY, SSHFP + Algorithm string `json:"algorithm,omitempty" bson:"algorithm,omitempty"` + + // LOC + Altitude string `json:"altitude,omitempty" bson:"altitude,omitempty"` + + // CNAME + CName string `json:"cname,omitempty" bson:"cname,omitempty"` + + // CERT + Certificate string `json:"certificate,omitempty" bson:"algorithm,omitempty"` + + // DNAME + DName string `json:"dname,omitempty" bson:"dname,omitempty"` + + // DHCID, DS + Digest string `json:"digest,omitempty" bson:"digest,omitempty"` + + // DS + DigestType string `json:"digtype,omitempty" bson:"digest_type,omitempty"` + + // KX, MX + Exchange string `json:"exchange,omitempty" bson:"exchange,omitempty"` + + // SSHFP + FPType string `json:"fptype,omitempty" bson:"fp_type,omitempty"` + + // SSHFP + Fingerprint string `json:"fingerprint,omitempty" bson:"fingerprint,omitempty"` + + // DNSKEY, KEY, NAPTR + Flags string `json:"flags,omitempty" bson:"flags,omitempty"` + + // CERT + Format string `json:"format,omitempty" bson:"format,omitempty"` + + // IPSECKEY + GatewayType string `json:"gatetype,omitempty" bson:"gateway_type,omitempty"` + + // LOC + HorizPre string `json:"horiz_pre,omitempty" bson:"horiz_pre,omitempty"` + + // DS + KeyTag string `json:"keytag,omitempty" bson:"keytag,omitempty"` + + // LOC + Latitude string `json:"latitude,omitempty" bson:"latitude,omitempty"` + + // LOC + Longitude string `json:"longitude,omitempty" bson:"longitude,omitempty"` + + // PX + Map822 string `json:"map822,omitempty" bson:"map_822,omitempty"` + + // PX + MapX400 string `json:"mapx400,omitempty" bson:"map_x400,omitempty"` + + // RP + Mbox string `json:"mbox,omitempty" bson:"mbox,omitempty"` + + // NS + NSDName string `json:"nsdname,omitempty" bson:"nsdname,omitempty"` + + // NSAP + NSAP string `json:"nsap,omitempty" bson:"nsap,omitempty"` + + // NAPTR + Order string `json:"order,omitempty" bson:"order,omitempty"` + + // SRV + Port string `json:"port,omitempty" bson:"port,omitempty"` + + // IPSECKEY + Precendence string `json:"precendence,omitempty" bson:"precendence,omitempty"` + + // KX, MX, NAPTR, PX + Preference int `json:"preference,omitempty" bson:"preference,omitempty"` + + // SRV + Priority int `json:"priority,omitempty" bson:"priority,omitempty"` + + // DNSKEY, KEY + Protocol string `json:"protocol,omitempty" bson:"protocol,omitempty"` + + // PTR + PTRDname string `json:"ptrdname,omitempty" bson:"ptrdname,omitempty"` + + // DNSKEY, IPSECKEY, KEY + PublicKey string `json:"public_key,omitempty" bson:"public_key,omitempty"` + + // NAPTR + Regexp string `json:"regexp,omitempty" bson:"regexp,omitempty"` + + // NAPTR + Replacement string `json:"replacement,omitempty" bson:"replacement,omitempty"` + + // SOA + RName string `json:"rname,omitempty" bson:"rname,omitempty"` + + // NAPTR + Services string `json:"services,omitempty" bson:"services,omitempty"` + + // LOC + Size string `json:"size,omitempty" bson:"size,omitempty"` + + // CERT + Tag string `json:"tag,omitempty" bson:"tag,omitempty"` + + // SRV + Target string `json:"target,omitempty" bson:"target,omitempty"` + + // RP + TxtDName string `json:"txtdname,omitempty" bson:"txtdname,omitempty"` + + // SPF, TXT + TxtData string `json:"txtdata,omitempty" bson:"txtdata,omitempty"` + + // LOC + Version string `json:"version,omitempty" bson:"version,omitempty"` + + // LOC + VertPre string `json:"vert_pre,omitempty" bson:"vert_pre,omitempty"` + + // SRV + Weight string `json:"weight,omitempty" bson:"weight,omitempty"` +} diff --git a/vendor/github.com/nesv/go-dynect/dynect/zone.go b/vendor/github.com/nesv/go-dynect/dynect/zone.go new file mode 100644 index 0000000000..70a9d8e21e --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/zone.go @@ -0,0 +1,9 @@ +package dynect + +// Zone struct to hold record details +type Zone struct { + Serial string + SerialStyle string + Zone string + Type string +} diff --git a/vendor/github.com/nesv/go-dynect/dynect/zones.go b/vendor/github.com/nesv/go-dynect/dynect/zones.go new file mode 100644 index 0000000000..0852c6cf00 --- /dev/null +++ b/vendor/github.com/nesv/go-dynect/dynect/zones.go @@ -0,0 +1,24 @@ +package dynect + +// ZonesResponse is used for holding the data returned by a call to +// "https://api.dynect.net/REST/Zone/". +type ZonesResponse struct { + ResponseBlock + Data []string `json:"data"` +} + +// ZoneResponse is used for holding the data returned by a call to +// "https://api.dynect.net/REST/Zone/ZONE_NAME". +type ZoneResponse struct { + ResponseBlock + Data ZoneDataBlock `json:"data"` +} + +// Type ZoneDataBlock is used as a nested struct, which holds the data for a +// zone returned by a call to "https://api.dynect.net/REST/Zone/ZONE_NAME". +type ZoneDataBlock struct { + Serial int `json:"serial"` + SerialStyle string `json:"serial_style"` + Zone string `json:"zone"` + ZoneType string `json:"zone_type"` +}