From cb5863344bb5e43b731436c0583bb59778ead7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20J=C3=BCttner?= Date: Fri, 16 Jun 2017 11:28:13 +0200 Subject: [PATCH] CloudFlare as a new provider (#140) * CloudFlare Provider * updating glide * gofmt cloudflare_test.go * Unset envs to test NewCloudFlareProvider * More tests * fix(cloudflare): fix compiler errors resulting from merge * Typo * Undo vendor changes * decrease api calls, fix some nits * Cloudflare iteration (#2) * reduce the number of API calls * match by type and name for record id * improve coverage and fix the bug with suitable zone * tests failed due to wrong formatting * add cloudflare integration to the main * vendor cloudflare deps * fix cloudflare zone detection + tests * fix conflicting test function names --- glide.lock | 8 +- glide.yaml | 2 + main.go | 10 +- pkg/apis/externaldns/types.go | 2 +- provider/cloudflare.go | 293 +++++++ provider/cloudflare_test.go | 475 ++++++++++ provider/digital_ocean_test.go | 2 +- .../cloudflare/cloudflare-go/.travis.yml | 28 + .../cloudflare/cloudflare-go/LICENSE | 26 + .../cloudflare/cloudflare-go/README.md | 96 ++ .../cloudflare/cloudflare-go/cloudflare.go | 168 ++++ .../cloudflare-go/cloudflare_test.go | 60 ++ .../cloudflare-go/cmd/flarectl/README.md | 34 + .../cloudflare-go/cmd/flarectl/flarectl.go | 822 ++++++++++++++++++ .../cloudflare/cloudflare-go/cpage.go | 29 + .../cloudflare/cloudflare-go/dns.go | 177 ++++ .../cloudflare/cloudflare-go/errors.go | 47 + .../cloudflare/cloudflare-go/ips.go | 48 + .../cloudflare/cloudflare-go/keyless.go | 57 ++ .../cloudflare/cloudflare-go/options.go | 39 + .../cloudflare/cloudflare-go/organizations.go | 42 + .../cloudflare-go/organizations_test.go | 66 ++ .../cloudflare/cloudflare-go/pagerules.go | 232 +++++ .../cloudflare/cloudflare-go/railgun.go | 311 +++++++ .../cloudflare/cloudflare-go/railgun_test.go | 621 +++++++++++++ .../cloudflare/cloudflare-go/ssl.go | 154 ++++ .../cloudflare/cloudflare-go/ssl_test.go | 377 ++++++++ .../cloudflare/cloudflare-go/user.go | 116 +++ .../cloudflare/cloudflare-go/user_test.go | 196 +++++ .../cloudflare/cloudflare-go/virtualdns.go | 130 +++ .../cloudflare/cloudflare-go/waf.go | 97 +++ .../cloudflare/cloudflare-go/zone.go | 521 +++++++++++ .../cloudflare/cloudflare-go/zone_test.go | 584 +++++++++++++ vendor/github.com/pkg/errors/.gitignore | 24 + vendor/github.com/pkg/errors/.travis.yml | 11 + vendor/github.com/pkg/errors/LICENSE | 23 + vendor/github.com/pkg/errors/README.md | 52 ++ vendor/github.com/pkg/errors/appveyor.yml | 32 + vendor/github.com/pkg/errors/bench_test.go | 59 ++ vendor/github.com/pkg/errors/errors.go | 269 ++++++ vendor/github.com/pkg/errors/errors_test.go | 226 +++++ vendor/github.com/pkg/errors/example_test.go | 205 +++++ vendor/github.com/pkg/errors/format_test.go | 535 ++++++++++++ vendor/github.com/pkg/errors/stack.go | 178 ++++ vendor/github.com/pkg/errors/stack_test.go | 292 +++++++ 45 files changed, 7768 insertions(+), 8 deletions(-) create mode 100644 provider/cloudflare.go create mode 100644 provider/cloudflare_test.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/.travis.yml create mode 100644 vendor/github.com/cloudflare/cloudflare-go/LICENSE create mode 100644 vendor/github.com/cloudflare/cloudflare-go/README.md create mode 100644 vendor/github.com/cloudflare/cloudflare-go/cloudflare.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/cloudflare_test.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/cmd/flarectl/README.md create mode 100644 vendor/github.com/cloudflare/cloudflare-go/cmd/flarectl/flarectl.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/cpage.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/dns.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/errors.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/ips.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/keyless.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/options.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/organizations.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/organizations_test.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/pagerules.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/railgun.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/railgun_test.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/ssl.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/ssl_test.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/user.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/user_test.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/virtualdns.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/waf.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/zone.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/zone_test.go create mode 100644 vendor/github.com/pkg/errors/.gitignore create mode 100644 vendor/github.com/pkg/errors/.travis.yml create mode 100644 vendor/github.com/pkg/errors/LICENSE create mode 100644 vendor/github.com/pkg/errors/README.md create mode 100644 vendor/github.com/pkg/errors/appveyor.yml create mode 100644 vendor/github.com/pkg/errors/bench_test.go create mode 100644 vendor/github.com/pkg/errors/errors.go create mode 100644 vendor/github.com/pkg/errors/errors_test.go create mode 100644 vendor/github.com/pkg/errors/example_test.go create mode 100644 vendor/github.com/pkg/errors/format_test.go create mode 100644 vendor/github.com/pkg/errors/stack.go create mode 100644 vendor/github.com/pkg/errors/stack_test.go diff --git a/glide.lock b/glide.lock index 90cbc5e68..dc7efcb9e 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 2cc9f8e5b9c63f3e474cb84c7f7d491a46c45ec4f12ad985544535c80834ac63 -updated: 2017-06-16T10:35:11.175866087+02:00 +hash: 9b0956761f95e461a9cbf8edfac1537ef55005b27f9be0659582b336a227b1c2 +updated: 2017-06-16T10:50:06.229451646+02:00 imports: - name: bitbucket.org/ww/goautoneg version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 @@ -60,6 +60,8 @@ imports: version: 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9 subpackages: - quantile +- name: github.com/cloudflare/cloudflare-go + version: 9d186a5948cb4bb3d1f3343bc6bcae91e7aa6479 - name: github.com/coreos/go-oidc version: be73733bb8cc830d0205609b95d125215f8e9c70 subpackages: @@ -147,6 +149,8 @@ imports: version: fc2b8d3a73c4867e51861bbdd5ae3c1f0869dd6a subpackages: - pbutil +- name: github.com/pkg/errors + version: 645ef00459ed84a119197bfb8d8205042c6df63d - name: github.com/pmezard/go-difflib version: d8ed2627bdf02c080bf22230dbb337003b7aba2d subpackages: diff --git a/glide.yaml b/glide.yaml index 06ec632dd..85b0e70f4 100644 --- a/glide.yaml +++ b/glide.yaml @@ -49,6 +49,8 @@ import: version: ~8.0.0 - package: github.com/dgrijalva/jwt-go version: ~3.0.0 +- package: github.com/cloudflare/cloudflare-go + version: ~0.7.3 - package: github.com/digitalocean/godo version: ~1.1.0 - package: github.com/coreos/go-oidc diff --git a/main.go b/main.go index dd528a124..d68087dee 100644 --- a/main.go +++ b/main.go @@ -110,14 +110,16 @@ func main() { var p provider.Provider switch cfg.Provider { - case "google": - p, err = provider.NewGoogleProvider(cfg.GoogleProject, cfg.DomainFilter, cfg.DryRun) case "aws": p, err = provider.NewAWSProvider(cfg.DomainFilter, cfg.DryRun) - case "digitalocean": - p, err = provider.NewDigitalOceanProvider(cfg.DomainFilter, cfg.DryRun) case "azure": p, err = provider.NewAzureProvider(cfg.AzureConfigFile, cfg.DomainFilter, cfg.AzureResourceGroup, cfg.DryRun) + case "cloudflare": + p, err = provider.NewCloudFlareProvider(cfg.DomainFilter, cfg.DryRun) + case "google": + p, err = provider.NewGoogleProvider(cfg.GoogleProject, cfg.DomainFilter, cfg.DryRun) + case "digitalocean": + p, err = provider.NewDigitalOceanProvider(cfg.DomainFilter, cfg.DryRun) case "inmemory": p, err = provider.NewInMemoryProvider(provider.InMemoryWithDomain(cfg.DomainFilter), provider.InMemoryWithLogging()), nil default: diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 110ea0a7f..5ee83e832 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -97,7 +97,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule") // Flags related to providers - app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, inmemory, azure, digitalocean)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "digitalocean", "azure", "inmemory") + app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "inmemory") app.Flag("google-project", "When using the Google provider, specify the Google project (required when --provider=google)").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject) app.Flag("domain-filter", "Limit possible target zones by a domain suffix (optional)").Default(defaultConfig.DomainFilter).StringVar(&cfg.DomainFilter) app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile) diff --git a/provider/cloudflare.go b/provider/cloudflare.go new file mode 100644 index 000000000..91b7ad556 --- /dev/null +++ b/provider/cloudflare.go @@ -0,0 +1,293 @@ +/* +Copyright 2017 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" + + log "github.com/Sirupsen/logrus" + + "github.com/cloudflare/cloudflare-go" + + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/kubernetes-incubator/external-dns/plan" +) + +const ( + // cloudFlareCreate is a ChangeAction enum value + cloudFlareCreate = "CREATE" + // cloudFlareDelete is a ChangeAction enum value + cloudFlareDelete = "DELETE" + // cloudFlareUpdate is a ChangeAction enum value + cloudFlareUpdate = "UPDATE" +) + +// cloudFlareDNS is the subset of the CloudFlare API that we actually use. Add methods as required. Signatures must match exactly. +type cloudFlareDNS interface { + UserDetails() (cloudflare.User, error) + ZoneIDByName(zoneName string) (string, error) + ListZones(zoneID ...string) ([]cloudflare.Zone, error) + DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) + CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) + DeleteDNSRecord(zoneID, recordID string) error + UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error +} + +type zoneService struct { + service *cloudflare.API +} + +func (z zoneService) UserDetails() (cloudflare.User, error) { + return z.service.UserDetails() +} + +func (z zoneService) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { + return z.service.ListZones(zoneID...) +} + +func (z zoneService) ZoneIDByName(zoneName string) (string, error) { + return z.service.ZoneIDByName(zoneName) +} + +func (z zoneService) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { + return z.service.CreateDNSRecord(zoneID, rr) +} + +func (z zoneService) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { + return z.service.DNSRecords(zoneID, rr) +} +func (z zoneService) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { + return z.service.UpdateDNSRecord(zoneID, recordID, rr) +} +func (z zoneService) DeleteDNSRecord(zoneID, recordID string) error { + return z.service.DeleteDNSRecord(zoneID, recordID) +} + +// CloudFlareProvider is an implementation of Provider for CloudFlare DNS. +type CloudFlareProvider struct { + Client cloudFlareDNS + // only consider hosted zones managing domains ending in this suffix + domainFilter string + DryRun bool +} + +// cloudFlareChange differentiates between ChangActions +type cloudFlareChange struct { + Action string + ResourceRecordSet cloudflare.DNSRecord +} + +// NewCloudFlareProvider initializes a new CloudFlare DNS based Provider. +func NewCloudFlareProvider(domainFilter string, dryRun bool) (*CloudFlareProvider, error) { + // initialize via API email and API key and returns new API object + config, err := cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL")) + if err != nil { + return nil, fmt.Errorf("failed to initialize cloudflare provider: %v", err) + } + provider := &CloudFlareProvider{ + //Client: config, + Client: zoneService{config}, + domainFilter: domainFilter, + DryRun: dryRun, + } + return provider, nil +} + +// Zones returns the list of hosted zones. +func (p *CloudFlareProvider) Zones() ([]cloudflare.Zone, error) { + result := []cloudflare.Zone{} + + zones, err := p.Client.ListZones() + if err != nil { + return nil, err + } + + for _, zone := range zones { + if strings.HasSuffix(zone.Name, p.domainFilter) { + result = append(result, zone) + } + } + + return result, nil +} + +// Records returns the list of records. +func (p *CloudFlareProvider) Records() ([]*endpoint.Endpoint, error) { + zones, err := p.Zones() + if err != nil { + return nil, err + } + + endpoints := []*endpoint.Endpoint{} + for _, zone := range zones { + records, err := p.Client.DNSRecords(zone.ID, cloudflare.DNSRecord{}) + if err != nil { + return nil, err + } + + for _, r := range records { + switch r.Type { + case "A", "CNAME", "TXT": + break + default: + continue + } + endpoints = append(endpoints, endpoint.NewEndpoint(r.Name, r.Content, r.Type)) + } + } + + return endpoints, nil +} + +// ApplyChanges applies a given set of changes in a given zone. +func (p *CloudFlareProvider) ApplyChanges(changes *plan.Changes) error { + combinedChanges := make([]*cloudFlareChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) + + combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareCreate, changes.Create)...) + combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareUpdate, changes.UpdateNew)...) + combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareDelete, changes.Delete)...) + + return p.submitChanges(combinedChanges) +} + +// submitChanges takes a zone and a collection of Changes and sends them as a single transaction. +func (p *CloudFlareProvider) submitChanges(changes []*cloudFlareChange) error { + // return early if there is nothing to change + if len(changes) == 0 { + return nil + } + + zones, err := p.Zones() + if err != nil { + return err + } + // separate into per-zone change sets to be passed to the API. + changesByZone := cloudflareChangesByZone(zones, changes) + + for zoneID, changes := range changesByZone { + records, err := p.Client.DNSRecords(zoneID, cloudflare.DNSRecord{}) + if err != nil { + return fmt.Errorf("could not fetch records from zone, %v", err) + } + for _, change := range changes { + logFields := log.Fields{ + "record": change.ResourceRecordSet.Name, + "type": change.ResourceRecordSet.Type, + "action": change.Action, + "zone": zoneID, + } + + log.WithFields(logFields).Info("Changing record.") + + if p.DryRun { + continue + } + recordID := p.getRecordID(records, change.ResourceRecordSet) + switch change.Action { + case cloudFlareCreate: + _, err := p.Client.CreateDNSRecord(zoneID, change.ResourceRecordSet) + if err != nil { + log.WithFields(logFields).Errorf("failed to create record: %v", err) + } + case cloudFlareDelete: + err := p.Client.DeleteDNSRecord(zoneID, recordID) + if err != nil { + log.WithFields(logFields).Errorf("failed to delete record: %v", err) + } + case cloudFlareUpdate: + err := p.Client.UpdateDNSRecord(zoneID, recordID, change.ResourceRecordSet) + if err != nil { + log.WithFields(logFields).Errorf("failed to update record: %v", err) + } + } + } + } + return nil +} + +// changesByZone separates a multi-zone change into a single change per zone. +func cloudflareChangesByZone(zones []cloudflare.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange { + changes := make(map[string][]*cloudFlareChange) + + for _, z := range zones { + changes[z.ID] = []*cloudFlareChange{} + } + + for _, c := range changeSet { + zone := cloudflareSuitableZone(c.ResourceRecordSet.Name, zones) + if zone == nil { + log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected ", c.ResourceRecordSet.Name) + continue + } + changes[zone.ID] = append(changes[zone.ID], c) + } + + return changes +} + +// cloudflareSuitableZone returns the most suitable zone for a given hostname +// and a set of zones. +func cloudflareSuitableZone(hostname string, zones []cloudflare.Zone) *cloudflare.Zone { + var result *cloudflare.Zone + for i := range zones { + zone := &zones[i] + if strings.HasSuffix(hostname, zone.Name) { + if result == nil || len(zone.Name) > len(result.Name) { + result = zone + } + } + } + return result +} + +func (p *CloudFlareProvider) getRecordID(records []cloudflare.DNSRecord, record cloudflare.DNSRecord) string { + for _, zoneRecord := range records { + if zoneRecord.Name == record.Name && zoneRecord.Type == record.Type { + return zoneRecord.ID + } + } + return "" +} + +// newCloudFlareChanges returns a collection of Changes based on the given records and action. +func newCloudFlareChanges(action string, endpoints []*endpoint.Endpoint) []*cloudFlareChange { + changes := make([]*cloudFlareChange, 0, len(endpoints)) + + for _, endpoint := range endpoints { + changes = append(changes, newCloudFlareChange(action, endpoint)) + } + + return changes +} + +func newCloudFlareChange(action string, endpoint *endpoint.Endpoint) *cloudFlareChange { + typ := suitableType(endpoint) + + return &cloudFlareChange{ + Action: action, + ResourceRecordSet: cloudflare.DNSRecord{ + Name: endpoint.DNSName, + // TTL Value of 1 is 'automatic' + TTL: 1, + Proxied: false, + Type: typ, + Content: endpoint.Target, + }, + } +} diff --git a/provider/cloudflare_test.go b/provider/cloudflare_test.go new file mode 100644 index 000000000..b9a4fb996 --- /dev/null +++ b/provider/cloudflare_test.go @@ -0,0 +1,475 @@ +/* +Copyright 2017 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" + "testing" + + "github.com/cloudflare/cloudflare-go" + + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/kubernetes-incubator/external-dns/plan" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockCloudFlareClient struct{} + +func (m *mockCloudFlareClient) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { + return nil, nil +} + +func (m *mockCloudFlareClient) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { + if zoneID == "1234567890" { + return []cloudflare.DNSRecord{ + {ID: "1234567890", Name: "foobar.ext-dns-test.zalando.to.", Type: "A"}, + {ID: "1231231233", Name: "foo.bar.com"}}, + nil + } + return nil, nil +} + +func (m *mockCloudFlareClient) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { + return nil +} + +func (m *mockCloudFlareClient) DeleteDNSRecord(zoneID, recordID string) error { + return nil +} + +func (m *mockCloudFlareClient) UserDetails() (cloudflare.User, error) { + return cloudflare.User{ID: "xxxxxxxxxxxxxxxxxxx"}, nil +} + +func (m *mockCloudFlareClient) ZoneIDByName(zoneName string) (string, error) { + return "1234567890", nil +} + +func (m *mockCloudFlareClient) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { + return []cloudflare.Zone{{ID: "1234567890", Name: "ext-dns-test.zalando.to."}, {ID: "1234567891", Name: "foo.com."}}, nil +} + +type mockCloudFlareUserDetailsFail struct{} + +func (m *mockCloudFlareUserDetailsFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { + return nil, nil +} + +func (m *mockCloudFlareUserDetailsFail) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { + return []cloudflare.DNSRecord{}, nil +} + +func (m *mockCloudFlareUserDetailsFail) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { + return nil +} + +func (m *mockCloudFlareUserDetailsFail) DeleteDNSRecord(zoneID, recordID string) error { + return nil +} + +func (m *mockCloudFlareUserDetailsFail) UserDetails() (cloudflare.User, error) { + return cloudflare.User{}, fmt.Errorf("could not get ID from zone name") +} + +func (m *mockCloudFlareUserDetailsFail) ZoneIDByName(zoneName string) (string, error) { + return "", nil +} + +func (m *mockCloudFlareUserDetailsFail) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { + return []cloudflare.Zone{{Name: "ext-dns-test.zalando.to."}}, nil +} + +type mockCloudFlareCreateZoneFail struct{} + +func (m *mockCloudFlareCreateZoneFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { + return nil, nil +} + +func (m *mockCloudFlareCreateZoneFail) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { + return []cloudflare.DNSRecord{}, nil +} + +func (m *mockCloudFlareCreateZoneFail) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { + return nil +} + +func (m *mockCloudFlareCreateZoneFail) DeleteDNSRecord(zoneID, recordID string) error { + return nil +} + +func (m *mockCloudFlareCreateZoneFail) UserDetails() (cloudflare.User, error) { + return cloudflare.User{ID: "xxxxxxxxxxxxxxxxxxx"}, nil +} + +func (m *mockCloudFlareCreateZoneFail) ZoneIDByName(zoneName string) (string, error) { + return "", nil +} + +func (m *mockCloudFlareCreateZoneFail) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { + return []cloudflare.Zone{{Name: "ext-dns-test.zalando.to."}}, nil +} + +type mockCloudFlareDNSRecordsFail struct{} + +func (m *mockCloudFlareDNSRecordsFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { + return nil, nil +} + +func (m *mockCloudFlareDNSRecordsFail) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { + return []cloudflare.DNSRecord{}, fmt.Errorf("can not get records from zone") +} +func (m *mockCloudFlareDNSRecordsFail) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { + return nil +} + +func (m *mockCloudFlareDNSRecordsFail) DeleteDNSRecord(zoneID, recordID string) error { + return nil +} + +func (m *mockCloudFlareDNSRecordsFail) UserDetails() (cloudflare.User, error) { + return cloudflare.User{ID: "xxxxxxxxxxxxxxxxxxx"}, nil +} + +func (m *mockCloudFlareDNSRecordsFail) ZoneIDByName(zoneName string) (string, error) { + return "", nil +} + +func (m *mockCloudFlareDNSRecordsFail) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { + return []cloudflare.Zone{{Name: "ext-dns-test.zalando.to."}}, nil +} + +type mockCloudFlareZoneIDByNameFail struct{} + +func (m *mockCloudFlareZoneIDByNameFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { + return nil, nil +} + +func (m *mockCloudFlareZoneIDByNameFail) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { + return []cloudflare.DNSRecord{}, nil +} + +func (m *mockCloudFlareZoneIDByNameFail) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { + return nil +} + +func (m *mockCloudFlareZoneIDByNameFail) DeleteDNSRecord(zoneID, recordID string) error { + return nil +} + +func (m *mockCloudFlareZoneIDByNameFail) UserDetails() (cloudflare.User, error) { + return cloudflare.User{}, nil +} + +func (m *mockCloudFlareZoneIDByNameFail) ZoneIDByName(zoneName string) (string, error) { + return "", fmt.Errorf("no ID for zone found") +} + +func (m *mockCloudFlareZoneIDByNameFail) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { + return []cloudflare.Zone{{Name: "ext-dns-test.zalando.to."}}, nil +} + +type mockCloudFlareDeleteZoneFail struct{} + +func (m *mockCloudFlareDeleteZoneFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { + return nil, nil +} + +func (m *mockCloudFlareDeleteZoneFail) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { + return []cloudflare.DNSRecord{}, nil +} + +func (m *mockCloudFlareDeleteZoneFail) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { + return nil +} + +func (m *mockCloudFlareDeleteZoneFail) DeleteDNSRecord(zoneID, recordID string) error { + return nil +} + +func (m *mockCloudFlareDeleteZoneFail) UserDetails() (cloudflare.User, error) { + return cloudflare.User{}, nil +} + +func (m *mockCloudFlareDeleteZoneFail) ZoneIDByName(zoneName string) (string, error) { + return "1234567890", nil +} + +func (m *mockCloudFlareDeleteZoneFail) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { + return []cloudflare.Zone{{Name: "ext-dns-test.zalando.to."}}, nil +} + +type mockCloudFlareListZonesFail struct{} + +func (m *mockCloudFlareListZonesFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { + return nil, nil +} + +func (m *mockCloudFlareListZonesFail) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { + return []cloudflare.DNSRecord{}, nil +} + +func (m *mockCloudFlareListZonesFail) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { + return nil +} + +func (m *mockCloudFlareListZonesFail) DeleteDNSRecord(zoneID, recordID string) error { + return nil +} + +func (m *mockCloudFlareListZonesFail) UserDetails() (cloudflare.User, error) { + return cloudflare.User{}, nil +} + +func (m *mockCloudFlareListZonesFail) ZoneIDByName(zoneName string) (string, error) { + return "1234567890", nil +} + +func (m *mockCloudFlareListZonesFail) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { + return []cloudflare.Zone{{}}, fmt.Errorf("no zones available") +} + +type mockCloudFlareCreateRecordsFail struct{} + +func (m *mockCloudFlareCreateRecordsFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { + return nil, fmt.Errorf("could not create record") +} + +func (m *mockCloudFlareCreateRecordsFail) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { + return []cloudflare.DNSRecord{{ID: "1234567890", Name: "foobar.ext-dns-test.zalando.to."}}, nil +} + +func (m *mockCloudFlareCreateRecordsFail) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { + return nil +} + +func (m *mockCloudFlareCreateRecordsFail) DeleteDNSRecord(zoneID, recordID string) error { + return nil +} + +func (m *mockCloudFlareCreateRecordsFail) UserDetails() (cloudflare.User, error) { + return cloudflare.User{ID: "xxxxxxxxxxxxxxxxxxx"}, nil +} + +func (m *mockCloudFlareCreateRecordsFail) ZoneIDByName(zoneName string) (string, error) { + return "1234567890", nil +} + +func (m *mockCloudFlareCreateRecordsFail) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { + return []cloudflare.Zone{{}}, fmt.Errorf("no zones available") +} + +type mockCloudFlareDeleteRecordsFail struct{} + +func (m *mockCloudFlareDeleteRecordsFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { + return nil, nil +} + +func (m *mockCloudFlareDeleteRecordsFail) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { + return []cloudflare.DNSRecord{{ID: "1234567890", Name: "foobar.ext-dns-test.zalando.to."}}, nil +} + +func (m *mockCloudFlareDeleteRecordsFail) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { + return nil +} + +func (m *mockCloudFlareDeleteRecordsFail) DeleteDNSRecord(zoneID, recordID string) error { + return fmt.Errorf("could not delete record") +} + +func (m *mockCloudFlareDeleteRecordsFail) UserDetails() (cloudflare.User, error) { + return cloudflare.User{ID: "xxxxxxxxxxxxxxxxxxx"}, nil +} + +func (m *mockCloudFlareDeleteRecordsFail) ZoneIDByName(zoneName string) (string, error) { + return "1234567890", nil +} + +func (m *mockCloudFlareDeleteRecordsFail) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { + return []cloudflare.Zone{{Name: "ext-dns-test.zalando.to."}}, nil +} + +type mockCloudFlareUpdateRecordsFail struct{} + +func (m *mockCloudFlareUpdateRecordsFail) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { + return nil, nil +} + +func (m *mockCloudFlareUpdateRecordsFail) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { + return []cloudflare.DNSRecord{{ID: "1234567890", Name: "foobar.ext-dns-test.zalando.to."}}, nil +} + +func (m *mockCloudFlareUpdateRecordsFail) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error { + return fmt.Errorf("could not update record") +} + +func (m *mockCloudFlareUpdateRecordsFail) DeleteDNSRecord(zoneID, recordID string) error { + return nil +} + +func (m *mockCloudFlareUpdateRecordsFail) UserDetails() (cloudflare.User, error) { + return cloudflare.User{ID: "xxxxxxxxxxxxxxxxxxx"}, nil +} + +func (m *mockCloudFlareUpdateRecordsFail) ZoneIDByName(zoneName string) (string, error) { + return "1234567890", nil +} + +func (m *mockCloudFlareUpdateRecordsFail) ListZones(zoneID ...string) ([]cloudflare.Zone, error) { + return []cloudflare.Zone{{Name: "ext-dns-test.zalando.to."}}, nil +} + +func TestNewCloudFlareChanges(t *testing.T) { + action := cloudFlareCreate + endpoints := []*endpoint.Endpoint{{DNSName: "new", Target: "target"}} + _ = newCloudFlareChanges(action, endpoints) +} + +func TestCloudFlareZones(t *testing.T) { + provider := &CloudFlareProvider{ + Client: &mockCloudFlareClient{}, + domainFilter: "zalando.to.", + } + + zones, err := provider.Zones() + if err != nil { + t.Fatal(err) + } + + validateCloudFlareZones(t, zones, []cloudflare.Zone{ + {Name: "ext-dns-test.zalando.to."}, + }) +} + +func TestRecords(t *testing.T) { + provider := &CloudFlareProvider{ + Client: &mockCloudFlareClient{}, + } + records, err := provider.Records() + if err != nil { + t.Errorf("should not fail, %s", err) + } + + assert.Equal(t, 1, len(records)) + provider.Client = &mockCloudFlareDNSRecordsFail{} + _, err = provider.Records() + if err == nil { + t.Errorf("expected to fail") + } + provider.Client = &mockCloudFlareListZonesFail{} + _, err = provider.Records() + if err == nil { + t.Errorf("expected to fail") + } +} + +func TestNewCloudFlareProvider(t *testing.T) { + _ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx") + _ = os.Setenv("CF_API_EMAIL", "test@test.com") + _, err := NewCloudFlareProvider("ext-dns-test.zalando.to.", true) + if err != nil { + t.Errorf("should not fail, %s", err) + } + _ = os.Unsetenv("CF_API_KEY") + _ = os.Unsetenv("CF_API_EMAIL") + _, err = NewCloudFlareProvider("ext-dns-test.zalando.to.", true) + if err == nil { + t.Errorf("expected to fail") + } +} + +func TestApplyChanges(t *testing.T) { + changes := &plan.Changes{} + provider := &CloudFlareProvider{ + Client: &mockCloudFlareClient{}, + } + changes.Create = []*endpoint.Endpoint{{DNSName: "new.ext-dns-test.zalando.to.", Target: "target"}, {DNSName: "new.ext-dns-test.unrelated.to.", Target: "target"}} + changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Target: "target"}} + changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Target: "target-old"}} + changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Target: "target-new"}} + err := provider.ApplyChanges(changes) + if err != nil { + t.Errorf("should not fail, %s", err) + } + + // empty changes + changes.Create = []*endpoint.Endpoint{} + changes.Delete = []*endpoint.Endpoint{} + changes.UpdateOld = []*endpoint.Endpoint{} + changes.UpdateNew = []*endpoint.Endpoint{} + + err = provider.ApplyChanges(changes) + if err != nil { + t.Errorf("should not fail, %s", err) + } +} + +func TestCloudFlareGetRecordID(t *testing.T) { + p := &CloudFlareProvider{} + records := []cloudflare.DNSRecord{ + { + Name: "foo.com", + Type: "CNAME", + ID: "1", + }, + { + Name: "bar.de", + Type: "A", + ID: "2", + }, + } + + assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{ + Name: "foo.com", + Type: "A", + })) + assert.Equal(t, "2", p.getRecordID(records, cloudflare.DNSRecord{ + Name: "bar.de", + Type: "A", + })) +} + +func TestCloudflareSuitableZone(t *testing.T) { + zones := []cloudflare.Zone{ + { + ID: "1", + Name: "foo.com", + }, + { + ID: "2", + Name: "foo.bar.com", + }, + { + ID: "3", + Name: "bar.com", + }, + } + hostname := "a.foo.bar.com" + zone := cloudflareSuitableZone(hostname, zones) + assert.NotNil(t, zone) + assert.Equal(t, "2", zone.ID) +} + +func validateCloudFlareZones(t *testing.T, zones []cloudflare.Zone, expected []cloudflare.Zone) { + require.Len(t, zones, len(expected)) + + for i, zone := range zones { + assert.Equal(t, expected[i].Name, zone.Name) + } +} diff --git a/provider/digital_ocean_test.go b/provider/digital_ocean_test.go index ce3241a49..2a57c7103 100644 --- a/provider/digital_ocean_test.go +++ b/provider/digital_ocean_test.go @@ -384,7 +384,7 @@ func (m *mockDigitalOceanCreateRecordsFail) Records(ctx context.Context, domain return []godo.DomainRecord{{ID: 1, Name: "foobar.ext-dns-test.zalando.to."}, {ID: 2}}, nil, nil } -func TestNewCloudFlareChanges(t *testing.T) { +func TestNewDigitalOceanChanges(t *testing.T) { action := DigitalOceanCreate endpoints := []*endpoint.Endpoint{{DNSName: "new", Target: "target"}} _ = newDigitalOceanChanges(action, endpoints) diff --git a/vendor/github.com/cloudflare/cloudflare-go/.travis.yml b/vendor/github.com/cloudflare/cloudflare-go/.travis.yml new file mode 100644 index 000000000..5bf0202de --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/.travis.yml @@ -0,0 +1,28 @@ +language: go +sudo: false + +matrix: + include: + - go: 1.4 + - go: 1.5 + - go: 1.6 + - go: 1.7 + - go: tip + allow_failures: + - go: tip + +install: + - if [[ $TRAVIS_GO_VERSION == 1.7* ]]; then go get github.com/golang/lint/golint; fi + +script: + - go get -t -v $(go list ./... | grep -v '/vendor/') + - if [[ $TRAVIS_GO_VERSION == 1.7* ]]; then diff -u <(echo -n) <(gofmt -d .); fi + - if [[ $TRAVIS_GO_VERSION == 1.7* ]]; then go vet $(go list ./... | grep -v '/vendor/'); fi + - if [[ $TRAVIS_GO_VERSION == 1.7* ]]; then for package in $(go list ./... | grep -v '/vendor/'); do golint -set_exit_status $package; done; fi + - go test -v -race $(go list ./... | grep -v '/vendor/') + +notifications: + email: + recipients: + - jamesog@cloudflare.com + - msilverlock@cloudflare.com diff --git a/vendor/github.com/cloudflare/cloudflare-go/LICENSE b/vendor/github.com/cloudflare/cloudflare-go/LICENSE new file mode 100644 index 000000000..c035bb48d --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2015-2016, Cloudflare. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/cloudflare/cloudflare-go/README.md b/vendor/github.com/cloudflare/cloudflare-go/README.md new file mode 100644 index 000000000..523e9b3e8 --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/README.md @@ -0,0 +1,96 @@ +# cloudflare-go + +[![GoDoc](https://img.shields.io/badge/godoc-reference-5673AF.svg?style=flat-square)](https://godoc.org/github.com/cloudflare/cloudflare-go) +[![Build Status](https://img.shields.io/travis/cloudflare/cloudflare-go/master.svg?style=flat-square)](https://travis-ci.org/cloudflare/cloudflare-go) +[![Go Report Card](https://goreportcard.com/badge/github.com/cloudflare/cloudflare-go?style=flat-square)](https://goreportcard.com/report/github.com/cloudflare/cloudflare-go) + +> **Note**: This library is under active development as we expand it to cover our (expanding!) API. +Consider the public API of this package a little unstable as we work towards a v1.0. + +A Go library for interacting with [Cloudflare's API v4](https://api.cloudflare.com/). This library +allows you to: + +* Manage and automate changes to your DNS records within Cloudflare +* Manage and automate changes to your zones (domains) on Cloudflare, including adding new zones to + your account +* List and modify the status of WAF (Web Application Firewall) rules for your zones +* Fetch Cloudflare's IP ranges for automating your firewall whitelisting + +A command-line client, [flarectl](cmd/flarectl), is also available as part of this project. + +## Features + +The current feature list includes: + +- [x] DNS Records +- [x] Zones +- [x] Web Application Firewall (WAF) +- [x] Cloudflare IPs +- [x] User Administration (partial) +- [x] Virtual DNS Management +- [ ] Organization Administration +- [ ] [Railgun](https://www.cloudflare.com/railgun/) administration +- [ ] [Keyless SSL](https://blog.cloudflare.com/keyless-ssl-the-nitty-gritty-technical-details/) +- [ ] [Origin CA](https://blog.cloudflare.com/universal-ssl-encryption-all-the-way-to-the-origin-for-free/) + +Pull Requests are welcome, but please open an issue (or comment in an existing issue) to discuss any +non-trivial changes before submitting code. + +## Installation + +You need a working Go environment. + +``` +go get github.com/cloudflare/cloudflare-go +``` + +## Getting Started + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/cloudflare/cloudflare-go" +) + +func main() { + // Construct a new API object + api, err := cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL")) + if err != nil { + log.Fatal(err) + } + + // Fetch user details on the account + u, err := api.UserDetails() + if err != nil { + log.Fatal(err) + } + // Print user details + fmt.Println(u) + + // Fetch the zone ID + id, err := api.ZoneIDByName("example.com") // Assuming example.com exists in your Cloudflare account already + if err != nil { + log.Fatal(err) + } + + // Fetch zone details + zone, err := api.ZoneDetails(id) + if err != nil { + log.Fatal(err) + } + // Print zone details + fmt.Println(zone) +} +``` + +Also refer to the [API documentation](https://godoc.org/github.com/cloudflare/cloudflare-go) for how +to use this package in-depth. + +# License + +BSD licensed. See the [LICENSE](LICENSE) file for details. diff --git a/vendor/github.com/cloudflare/cloudflare-go/cloudflare.go b/vendor/github.com/cloudflare/cloudflare-go/cloudflare.go new file mode 100644 index 000000000..16b8cd6bb --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/cloudflare.go @@ -0,0 +1,168 @@ +// Package cloudflare implements the Cloudflare v4 API. +package cloudflare + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "net/http" + + "github.com/pkg/errors" +) + +const apiURL = "https://api.cloudflare.com/client/v4" + +// API holds the configuration for the current API client. A client should not +// be modified concurrently. +type API struct { + APIKey string + APIEmail string + BaseURL string + headers http.Header + httpClient *http.Client +} + +// New creates a new Cloudflare v4 API client. +func New(key, email string, opts ...Option) (*API, error) { + if key == "" || email == "" { + return nil, errors.New(errEmptyCredentials) + } + + api := &API{ + APIKey: key, + APIEmail: email, + BaseURL: apiURL, + headers: make(http.Header), + } + + err := api.parseOptions(opts...) + if err != nil { + return nil, errors.Wrap(err, "options parsing failed") + } + + // Fall back to http.DefaultClient if the package user does not provide + // their own. + if api.httpClient == nil { + api.httpClient = http.DefaultClient + } + + return api, nil +} + +// ZoneIDByName retrieves a zone's ID from the name. +func (api *API) ZoneIDByName(zoneName string) (string, error) { + res, err := api.ListZones(zoneName) + if err != nil { + return "", errors.Wrap(err, "ListZones command failed") + } + for _, zone := range res { + if zone.Name == zoneName { + return zone.ID, nil + } + } + return "", errors.New("Zone could not be found") +} + +// makeRequest makes a HTTP request and returns the body as a byte slice, +// closing it before returnng. params will be serialized to JSON. +func (api *API) makeRequest(method, uri string, params interface{}) ([]byte, error) { + // Replace nil with a JSON object if needed + var reqBody io.Reader + if params != nil { + json, err := json.Marshal(params) + if err != nil { + return nil, errors.Wrap(err, "error marshalling params to JSON") + } + reqBody = bytes.NewReader(json) + } else { + reqBody = nil + } + + resp, err := api.request(method, uri, reqBody) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "could not read response body") + } + + switch resp.StatusCode { + case http.StatusOK: + break + case http.StatusUnauthorized: + return nil, errors.Errorf("HTTP status %d: invalid credentials", resp.StatusCode) + case http.StatusForbidden: + return nil, errors.Errorf("HTTP status %d: insufficient permissions", resp.StatusCode) + case http.StatusServiceUnavailable, http.StatusBadGateway, http.StatusGatewayTimeout, + 522, 523, 524: + return nil, errors.Errorf("HTTP status %d: service failure", resp.StatusCode) + default: + var s string + if body != nil { + s = string(body) + } + return nil, errors.Errorf("HTTP status %d: content %q", resp.StatusCode, s) + } + + return body, nil +} + +// request makes a HTTP request to the given API endpoint, returning the raw +// *http.Response, or an error if one occurred. The caller is responsible for +// closing the response body. +func (api *API) request(method, uri string, reqBody io.Reader) (*http.Response, error) { + req, err := http.NewRequest(method, api.BaseURL+uri, reqBody) + if err != nil { + return nil, errors.Wrap(err, "HTTP request creation failed") + } + + // Apply any user-defined headers first. + req.Header = cloneHeader(api.headers) + req.Header.Set("X-Auth-Key", api.APIKey) + req.Header.Set("X-Auth-Email", api.APIEmail) + + resp, err := api.httpClient.Do(req) + if err != nil { + return nil, errors.Wrap(err, "HTTP request failed") + } + + return resp, nil +} + +// cloneHeader returns a shallow copy of the header. +// copied from https://godoc.org/github.com/golang/gddo/httputil/header#Copy +func cloneHeader(header http.Header) http.Header { + h := make(http.Header) + for k, vs := range header { + h[k] = vs + } + return h +} + +// ResponseInfo contains a code and message returned by the API as errors or +// informational messages inside the response. +type ResponseInfo struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// Response is a template. There will also be a result struct. There will be a +// unique response type for each response, which will include this type. +type Response struct { + Success bool `json:"success"` + Errors []ResponseInfo `json:"errors"` + Messages []ResponseInfo `json:"messages"` +} + +// ResultInfo contains metadata about the Response. +type ResultInfo struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + TotalPages int `json:"total_pages"` + Count int `json:"count"` + Total int `json:"total_count"` +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/cloudflare_test.go b/vendor/github.com/cloudflare/cloudflare-go/cloudflare_test.go new file mode 100644 index 000000000..9658d2e9d --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/cloudflare_test.go @@ -0,0 +1,60 @@ +package cloudflare + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + // mux is the HTTP request multiplexer used with the test server. + mux *http.ServeMux + + // client is the API client being tested + client *API + + // server is a test HTTP server used to provide mock API responses + server *httptest.Server +) + +func setup() { + // test server + mux = http.NewServeMux() + server = httptest.NewServer(mux) + + // Cloudflare client configured to use test server + client, _ = New("cloudflare@example.org", "deadbeef") + client.BaseURL = server.URL +} + +func teardown() { + server.Close() +} + +func TestClient_Auth(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/ips", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method, "Expected method 'GET', got %s", r.Method) + assert.Equal(t, "cloudflare@example.com", r.Header.Get("X-Auth-Email")) + assert.Equal(t, "deadbeef", r.Header.Get("X-Auth-Token")) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "response": { + "ipv4_cidrs": ["199.27.128.0/21"], + "ipv6_cidrs": ["199.27.128.0/21"] + } +}`) + }) + + _, err := IPs() + + assert.NoError(t, err) +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/cmd/flarectl/README.md b/vendor/github.com/cloudflare/cloudflare-go/cmd/flarectl/README.md new file mode 100644 index 000000000..9417f9e0b --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/cmd/flarectl/README.md @@ -0,0 +1,34 @@ +# flarectl + +A CLI application for interacting with a Cloudflare account. + +# Usage + +You must set your API key and account email address in the environment variables `CF_API_KEY` and `CF_API_EMAIL`. + +``` +$ export CF_API_KEY=abcdef1234567890 +$ export CF_API_EMAIL=someone@example.com +$ flarectl +NAME: + flarectl - Cloudflare CLI + +USAGE: + flarectl [global options] command [command options] [arguments...] + +VERSION: + 2015.12.0 + +COMMANDS: + user, u User information + zone, z Zone information + dns, d DNS records + railgun, r Railgun information + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --help, -h show help + --version, -v print the version +``` + + diff --git a/vendor/github.com/cloudflare/cloudflare-go/cmd/flarectl/flarectl.go b/vendor/github.com/cloudflare/cloudflare-go/cmd/flarectl/flarectl.go new file mode 100644 index 000000000..df018b5bb --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/cmd/flarectl/flarectl.go @@ -0,0 +1,822 @@ +package main + +import ( + "errors" + "fmt" + "log" + "os" + "reflect" + "strings" + + "github.com/cloudflare/cloudflare-go" + "github.com/codegangsta/cli" +) + +var api *cloudflare.API + +// Map type used for printing a table +type table map[string]string + +// Print a nicely-formatted table +func makeTable(zones []table, cols ...string) { + // Store the maximum length of all columns + // The default is the length of the title + lens := make(map[string]int) + for _, col := range cols { + lens[col] = len(col) + } + // Increase the size of the column if it is larger than the current value + for _, z := range zones { + for col, val := range z { + if _, ok := lens[col]; ok && len(val) > lens[col] { + lens[col] = len(val) + } + } + } + // Print the headings and an underline for each heading + for _, col := range cols { + fmt.Printf("%s%s ", strings.Title(col), strings.Repeat(" ", lens[col]-len(col))) + } + fmt.Println() + for _, col := range cols { + fmt.Printf("%s ", strings.Repeat("-", lens[col])) + } + fmt.Println() + // And finally print the table data + for _, z := range zones { + for _, col := range cols { + fmt.Printf("%s%s ", z[col], strings.Repeat(" ", lens[col]-len(z[col]))) + } + fmt.Println() + } + +} + +func checkEnv() error { + if api == nil { + var err error + api, err = cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL")) + if err != nil { + log.Fatal(err) + } + } + + if api.APIKey == "" { + return errors.New("API key not defined") + } + if api.APIEmail == "" { + return errors.New("API email not defined") + } + + return nil +} + +// Utility function to check if CLI flags were given. +func checkFlags(c *cli.Context, flags ...string) error { + for _, flag := range flags { + if c.String(flag) == "" { + cli.ShowSubcommandHelp(c) + return fmt.Errorf("%s not specified", flag) + } + } + return nil +} + +func ips(*cli.Context) { + ips, _ := cloudflare.IPs() + fmt.Println("IPv4 ranges:") + for _, r := range ips.IPv4CIDRs { + fmt.Println(" ", r) + } + fmt.Println() + fmt.Println("IPv6 ranges:") + for _, r := range ips.IPv6CIDRs { + fmt.Println(" ", r) + } +} + +func userInfo(*cli.Context) { + if err := checkEnv(); err != nil { + fmt.Println(err) + return + } + user, err := api.UserDetails() + if err != nil { + fmt.Println(err) + return + } + var output []table + output = append(output, table{ + "ID": user.ID, + "Email": user.Email, + "Username": user.Username, + "Name": user.FirstName + " " + user.LastName, + "2FA": fmt.Sprintf("%t", user.TwoFA), + }) + makeTable(output, "ID", "Email", "Username", "Name", "2FA") +} + +func userUpdate(*cli.Context) { +} + +func zoneCreate(c *cli.Context) { + if err := checkEnv(); err != nil { + fmt.Println(err) + return + } + if err := checkFlags(c, "zone"); err != nil { + return + } + zone := c.String("zone") + jumpstart := c.Bool("jumpstart") + orgID := c.String("org-id") + var org cloudflare.Organization + if orgID != "" { + org.ID = orgID + } + api.CreateZone(zone, jumpstart, org) +} + +func zoneCheck(c *cli.Context) { + if err := checkEnv(); err != nil { + fmt.Println(err) + return + } + if err := checkFlags(c, "zone"); err != nil { + return + } + zone := c.String("zone") + + zoneID, err := api.ZoneIDByName(zone) + if err != nil { + fmt.Println(err) + return + } + + res, err := api.ZoneActivationCheck(zoneID) + if err != nil { + fmt.Println(err) + return + } + fmt.Printf("%s\n", res.Messages[0].Message) +} + +func zoneList(c *cli.Context) { + if err := checkEnv(); err != nil { + fmt.Println(err) + return + } + zones, err := api.ListZones() + if err != nil { + fmt.Println(err) + return + } + var output []table + for _, z := range zones { + output = append(output, table{ + "ID": z.ID, + "Name": z.Name, + "Plan": z.Plan.LegacyID, + "Status": z.Status, + }) + } + makeTable(output, "ID", "Name", "Plan", "Status") +} + +func zoneInfo(c *cli.Context) { + if err := checkEnv(); err != nil { + fmt.Println(err) + return + } + var zone string + if len(c.Args()) > 0 { + zone = c.Args()[0] + } else if c.String("zone") != "" { + zone = c.String("zone") + } else { + cli.ShowSubcommandHelp(c) + return + } + zones, err := api.ListZones(zone) + if err != nil { + fmt.Println(err) + return + } + var output []table + for _, z := range zones { + var nameservers []string + if len(z.VanityNS) > 0 { + nameservers = z.VanityNS + } else { + nameservers = z.NameServers + } + output = append(output, table{ + "ID": z.ID, + "Zone": z.Name, + "Plan": z.Plan.LegacyID, + "Status": z.Status, + "Name Servers": strings.Join(nameservers, ", "), + "Paused": fmt.Sprintf("%t", z.Paused), + "Type": z.Type, + }) + } + makeTable(output, "ID", "Zone", "Plan", "Status", "Name Servers", "Paused", "Type") +} + +func zonePlan(*cli.Context) { +} + +func zoneSettings(*cli.Context) { +} + +func zoneRecords(c *cli.Context) { + if err := checkEnv(); err != nil { + fmt.Println(err) + return + } + var zone string + if len(c.Args()) > 0 { + zone = c.Args()[0] + } else if c.String("zone") != "" { + zone = c.String("zone") + } else { + cli.ShowSubcommandHelp(c) + return + } + + zoneID, err := api.ZoneIDByName(zone) + if err != nil { + fmt.Println(err) + return + } + + // Create a an empty record for searching for records + rr := cloudflare.DNSRecord{} + var records []cloudflare.DNSRecord + if c.String("id") != "" { + rec, err := api.DNSRecord(zoneID, c.String("id")) + if err != nil { + fmt.Println(err) + return + } + records = append(records, rec) + } else { + if c.String("name") != "" { + rr.Name = c.String("name") + } + if c.String("content") != "" { + rr.Name = c.String("content") + } + var err error + records, err = api.DNSRecords(zoneID, rr) + if err != nil { + fmt.Println(err) + return + } + } + var output []table + for _, r := range records { + switch r.Type { + case "MX": + r.Content = fmt.Sprintf("%d %s", r.Priority, r.Content) + case "SRV": + dp := reflect.ValueOf(r.Data).Interface().(map[string]interface{}) + r.Content = fmt.Sprintf("%.f %s", dp["priority"], r.Content) + // Cloudflare's API, annoyingly, automatically prepends the weight + // and port into content, separated by tabs. + // XXX: File this as a bug. LOC doesn't do this. + r.Content = strings.Replace(r.Content, "\t", " ", -1) + } + output = append(output, table{ + "ID": r.ID, + "Type": r.Type, + "Name": r.Name, + "Content": r.Content, + "Proxied": fmt.Sprintf("%t", r.Proxied), + "TTL": fmt.Sprintf("%d", r.TTL), + }) + } + makeTable(output, "ID", "Type", "Name", "Content", "Proxied", "TTL") +} + +func dnsCreate(c *cli.Context) { + if err := checkEnv(); err != nil { + fmt.Println(err) + return + } + if err := checkFlags(c, "zone", "name", "type", "content"); err != nil { + return + } + zone := c.String("zone") + name := c.String("name") + rtype := c.String("type") + content := c.String("content") + ttl := c.Int("ttl") + proxy := c.Bool("proxy") + + zoneID, err := api.ZoneIDByName(zone) + if err != nil { + fmt.Println(err) + return + } + + record := cloudflare.DNSRecord{ + Name: name, + Type: strings.ToUpper(rtype), + Content: content, + TTL: ttl, + Proxied: proxy, + } + // TODO: Print the result. + _, err = api.CreateDNSRecord(zoneID, record) + if err != nil { + fmt.Println("Error creating DNS record:", err) + } +} + +func dnsCreateOrUpdate(c *cli.Context) { + if err := checkEnv(); err != nil { + fmt.Println(err) + return + } + if err := checkFlags(c, "zone", "name", "type", "content"); err != nil { + return + } + zone := c.String("zone") + name := c.String("name") + rtype := strings.ToUpper(c.String("type")) + content := c.String("content") + ttl := c.Int("ttl") + proxy := c.Bool("proxy") + + zoneID, err := api.ZoneIDByName(zone) + if err != nil { + fmt.Println(err) + return + } + + // Look for an existing record + rr := cloudflare.DNSRecord{ + Name: name + "." + zone, + } + records, err := api.DNSRecords(zoneID, rr) + if err != nil { + fmt.Println(err) + return + } + + if len(records) > 0 { + // Record exists - find the ID and update it. + // This is imprecise without knowing the original content; if a label + // has multiple RRs we'll just update the first one. + for _, r := range records { + if r.Type == rtype { + rr.ID = r.ID + rr.Type = r.Type + rr.Content = content + rr.TTL = ttl + rr.Proxied = proxy + err := api.UpdateDNSRecord(zoneID, r.ID, rr) + if err != nil { + fmt.Println("Error updating DNS record:", err) + } + } + } + } else { + // Record doesn't exist - create it + rr.Type = rtype + rr.Content = content + rr.TTL = ttl + rr.Proxied = proxy + // TODO: Print the response. + _, err := api.CreateDNSRecord(zoneID, rr) + if err != nil { + fmt.Println("Error creating DNS record:", err) + } + } +} + +func dnsUpdate(c *cli.Context) { + if err := checkEnv(); err != nil { + fmt.Println(err) + return + } + if err := checkFlags(c, "zone", "id"); err != nil { + return + } + zone := c.String("zone") + recordID := c.String("id") + name := c.String("name") + content := c.String("content") + ttl := c.Int("ttl") + proxy := c.Bool("proxy") + + zoneID, err := api.ZoneIDByName(zone) + if err != nil { + fmt.Println(err) + return + } + + record := cloudflare.DNSRecord{ + ID: recordID, + Name: name, + Content: content, + TTL: ttl, + Proxied: proxy, + } + err = api.UpdateDNSRecord(zoneID, recordID, record) + if err != nil { + fmt.Println("Error updating DNS record:", err) + } +} + +func dnsDelete(c *cli.Context) { + if err := checkEnv(); err != nil { + fmt.Println(err) + return + } + if err := checkFlags(c, "zone", "id"); err != nil { + return + } + zone := c.String("zone") + recordID := c.String("id") + + zoneID, err := api.ZoneIDByName(zone) + if err != nil { + fmt.Println(err) + return + } + + err = api.DeleteDNSRecord(zoneID, recordID) + if err != nil { + fmt.Println("Error deleting DNS record:", err) + } +} + +func zoneCerts(*cli.Context) { +} + +func zoneKeyless(*cli.Context) { +} + +func zoneRailgun(*cli.Context) { +} + +func pageRules(c *cli.Context) { + if err := checkEnv(); err != nil { + fmt.Println(err) + return + } + if err := checkFlags(c, "zone"); err != nil { + return + } + zone := c.String("zone") + + zoneID, err := api.ZoneIDByName(zone) + if err != nil { + fmt.Println(err) + return + } + + rules, err := api.ListPageRules(zoneID) + if err != nil { + fmt.Println(err) + return + } + + fmt.Printf("%3s %-32s %-8s %s\n", "Pri", "ID", "Status", "URL") + for _, r := range rules { + var settings []string + fmt.Printf("%3d %s %-8s %s\n", r.Priority, r.ID, r.Status, r.Targets[0].Constraint.Value) + for _, a := range r.Actions { + v := reflect.ValueOf(a.Value) + var s string + switch a.Value.(type) { + case int: + s = fmt.Sprintf("%s: %d", cloudflare.PageRuleActions[a.ID], v.Int()) + case float64: + s = fmt.Sprintf("%s: %.f", cloudflare.PageRuleActions[a.ID], v.Float()) + case map[string]interface{}: + vmap := a.Value.(map[string]interface{}) + s = fmt.Sprintf("%s: %.f - %s", cloudflare.PageRuleActions[a.ID], vmap["status_code"], vmap["url"]) + case nil: + s = fmt.Sprintf("%s", cloudflare.PageRuleActions[a.ID]) + default: + s = fmt.Sprintf("%s: %s", cloudflare.PageRuleActions[a.ID], strings.Title(strings.Replace(v.String(), "_", " ", -1))) + } + settings = append(settings, s) + } + fmt.Println(" ", strings.Join(settings, ", ")) + } +} + +func railgun(*cli.Context) { +} + +func main() { + app := cli.NewApp() + app.Name = "flarectl" + app.Usage = "Cloudflare CLI" + app.Version = "2016.4.0" + app.Commands = []cli.Command{ + { + Name: "ips", + Aliases: []string{"i"}, + Action: ips, + Usage: "Print Cloudflare IP ranges", + }, + { + Name: "user", + Aliases: []string{"u"}, + Usage: "User information", + Subcommands: []cli.Command{ + { + Name: "info", + Aliases: []string{"i"}, + Action: userInfo, + Usage: "User details", + }, + { + Name: "update", + Aliases: []string{"u"}, + Action: userUpdate, + Usage: "Update user details", + }, + }, + }, + + { + Name: "zone", + Aliases: []string{"z"}, + Usage: "Zone information", + Subcommands: []cli.Command{ + { + Name: "list", + Aliases: []string{"l"}, + Action: zoneList, + Usage: "List all zones on an account", + }, + { + Name: "create", + Aliases: []string{"c"}, + Action: zoneCreate, + Usage: "Create a new zone", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "zone", + Usage: "zone name", + }, + cli.BoolFlag{ + Name: "jumpstart", + Usage: "automatically fetch DNS records", + }, + cli.StringFlag{ + Name: "org-id", + Usage: "organization ID", + }, + }, + }, + { + Name: "check", + Action: zoneCheck, + Usage: "Initiate a zone activation check", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "zone", + Usage: "zone name", + }, + }, + }, + { + Name: "info", + Aliases: []string{"i"}, + Action: zoneInfo, + Usage: "Information on one zone", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "zone", + Usage: "zone name", + }, + }, + }, + { + Name: "plan", + Aliases: []string{"p"}, + Action: zonePlan, + Usage: "Plan information for one zone", + }, + { + Name: "settings", + Aliases: []string{"s"}, + Action: zoneSettings, + Usage: "Settings for one zone", + }, + { + Name: "dns", + Aliases: []string{"d"}, + Action: zoneRecords, + Usage: "DNS records for a zone", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "zone", + Usage: "zone name", + }, + }, + }, + { + Name: "railgun", + Aliases: []string{"r"}, + Action: zoneRailgun, + Usage: "Railguns for a zone", + }, + { + Name: "certs", + Aliases: []string{"c"}, + Action: zoneCerts, + Usage: "Custom SSL certificates for a zone", + }, + { + Name: "keyless", + Aliases: []string{"k"}, + Action: zoneKeyless, + Usage: "Keyless SSL for a zone", + }, + }, + }, + + { + Name: "dns", + Aliases: []string{"d"}, + Usage: "DNS records", + Subcommands: []cli.Command{ + { + Name: "list", + Aliases: []string{"l"}, + Action: zoneRecords, + Usage: "List DNS records for a zone", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Usage: "record id", + }, + cli.StringFlag{ + Name: "zone", + Usage: "zone name", + }, + cli.StringFlag{ + Name: "name", + Usage: "record name", + }, + cli.StringFlag{ + Name: "content", + Usage: "record content", + }, + }, + }, + { + Name: "create", + Aliases: []string{"c"}, + Action: dnsCreate, + Usage: "Create a DNS record", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "zone", + Usage: "zone name", + }, + cli.StringFlag{ + Name: "name", + Usage: "record name", + }, + cli.StringFlag{ + Name: "type", + Usage: "record type", + }, + cli.StringFlag{ + Name: "content", + Usage: "record content", + }, + cli.IntFlag{ + Name: "ttl", + Usage: "TTL (1 = automatic)", + Value: 1, + }, + cli.BoolFlag{ + Name: "proxy", + Usage: "proxy through Cloudflare (orange cloud)", + }, + }, + }, + { + Name: "update", + Aliases: []string{"u"}, + Action: dnsUpdate, + Usage: "Update a DNS record", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "zone", + Usage: "zone name", + }, + cli.StringFlag{ + Name: "id", + Usage: "record id", + }, + cli.StringFlag{ + Name: "name", + Usage: "record name", + }, + cli.StringFlag{ + Name: "content", + Usage: "record content", + }, + cli.IntFlag{ + Name: "ttl", + Usage: "TTL (1 = automatic)", + Value: 1, + }, + cli.BoolFlag{ + Name: "proxy", + Usage: "proxy through Cloudflare (orange cloud)", + }, + }, + }, + { + Name: "create-or-update", + Aliases: []string{"o"}, + Action: dnsCreateOrUpdate, + Usage: "Create a DNS record, or update if it exists", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "zone", + Usage: "zone name", + }, + cli.StringFlag{ + Name: "name", + Usage: "record name", + }, + cli.StringFlag{ + Name: "content", + Usage: "record content", + }, + cli.StringFlag{ + Name: "type", + Usage: "record type", + }, + cli.IntFlag{ + Name: "ttl", + Usage: "TTL (1 = automatic)", + Value: 1, + }, + cli.BoolFlag{ + Name: "proxy", + Usage: "proxy through Cloudflare (orange cloud)", + }, + }, + }, + { + Name: "delete", + Aliases: []string{"d"}, + Action: dnsDelete, + Usage: "Delete a DNS record", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "zone", + Usage: "zone name", + }, + cli.StringFlag{ + Name: "id", + Usage: "record id", + }, + }, + }, + }, + }, + + { + Name: "pagerules", + Aliases: []string{"p"}, + Usage: "Page Rules", + Subcommands: []cli.Command{ + { + Name: "list", + Aliases: []string{"l"}, + Action: pageRules, + Usage: "List Page Rules for a zone", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "zone", + Usage: "zone name", + }, + }, + }, + }, + }, + + { + Name: "railgun", + Aliases: []string{"r"}, + Usage: "Railgun information", + Action: railgun, + }, + } + app.Run(os.Args) +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/cpage.go b/vendor/github.com/cloudflare/cloudflare-go/cpage.go new file mode 100644 index 000000000..87e50ce84 --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/cpage.go @@ -0,0 +1,29 @@ +package cloudflare + +import "time" + +// CustomPage represents a custom page configuration. +type CustomPage struct { + CreatedOn string `json:"created_on"` + ModifiedOn time.Time `json:"modified_on"` + URL string `json:"url"` + State string `json:"state"` + RequiredTokens []string `json:"required_tokens"` + PreviewTarget string `json:"preview_target"` + Description string `json:"description"` +} + +// CustomPageResponse represents the response from the custom pages endpoint. +type CustomPageResponse struct { + Response + Result []CustomPage `json:"result"` +} + +// https://api.cloudflare.com/#custom-pages-for-a-zone-available-custom-pages +// GET /zones/:zone_identifier/custom_pages + +// https://api.cloudflare.com/#custom-pages-for-a-zone-custom-page-details +// GET /zones/:zone_identifier/custom_pages/:identifier + +// https://api.cloudflare.com/#custom-pages-for-a-zone-update-custom-page-url +// PUT /zones/:zone_identifier/custom_pages/:identifier diff --git a/vendor/github.com/cloudflare/cloudflare-go/dns.go b/vendor/github.com/cloudflare/cloudflare-go/dns.go new file mode 100644 index 000000000..6d10ca6fe --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/dns.go @@ -0,0 +1,177 @@ +package cloudflare + +import ( + "encoding/json" + "net/url" + "strconv" + "time" + + "github.com/pkg/errors" +) + +// DNSRecord represents a DNS record in a zone. +type DNSRecord struct { + ID string `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Content string `json:"content,omitempty"` + Proxiable bool `json:"proxiable,omitempty"` + Proxied bool `json:"proxied,omitempty"` + TTL int `json:"ttl,omitempty"` + Locked bool `json:"locked,omitempty"` + ZoneID string `json:"zone_id,omitempty"` + ZoneName string `json:"zone_name,omitempty"` + CreatedOn time.Time `json:"created_on,omitempty"` + ModifiedOn time.Time `json:"modified_on,omitempty"` + Data interface{} `json:"data,omitempty"` // data returned by: SRV, LOC + Meta interface{} `json:"meta,omitempty"` + Priority int `json:"priority,omitempty"` +} + +// DNSRecordResponse represents the response from the DNS endpoint. +type DNSRecordResponse struct { + Result DNSRecord `json:"result"` + Response + ResultInfo `json:"result_info"` +} + +// DNSListResponse represents the response from the list DNS records endpoint. +type DNSListResponse struct { + Result []DNSRecord `json:"result"` + Response + ResultInfo `json:"result_info"` +} + +// CreateDNSRecord creates a DNS record for the zone identifier. +// API reference: +// https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record +// POST /zones/:zone_identifier/dns_records +func (api *API) CreateDNSRecord(zoneID string, rr DNSRecord) (*DNSRecordResponse, error) { + uri := "/zones/" + zoneID + "/dns_records" + res, err := api.makeRequest("POST", uri, rr) + if err != nil { + return nil, errors.Wrap(err, errMakeRequestError) + } + + var recordResp *DNSRecordResponse + err = json.Unmarshal(res, &recordResp) + if err != nil { + return nil, errors.Wrap(err, errUnmarshalError) + } + + return recordResp, nil +} + +// DNSRecords returns a slice of DNS records for the given zone identifier. +// API reference: +// https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records +// GET /zones/:zone_identifier/dns_records +func (api *API) DNSRecords(zoneID string, rr DNSRecord) ([]DNSRecord, error) { + // Construct a query string + v := url.Values{} + // Request as many records as possible per page - API max is 50 + v.Set("per_page", "50") + if rr.Name != "" { + v.Set("name", rr.Name) + } + if rr.Type != "" { + v.Set("type", rr.Type) + } + if rr.Content != "" { + v.Set("content", rr.Content) + } + + var query string + var records []DNSRecord + page := 1 + + // Loop over makeRequest until what we've fetched all records + for { + v.Set("page", strconv.Itoa(page)) + query = "?" + v.Encode() + uri := "/zones/" + zoneID + "/dns_records" + query + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return []DNSRecord{}, errors.Wrap(err, errMakeRequestError) + } + var r DNSListResponse + err = json.Unmarshal(res, &r) + if err != nil { + return []DNSRecord{}, errors.Wrap(err, errUnmarshalError) + } + records = append(records, r.Result...) + if r.ResultInfo.Page >= r.ResultInfo.TotalPages { + break + } + // Loop around and fetch the next page + page++ + } + return records, nil +} + +// DNSRecord returns a single DNS record for the given zone & record +// identifiers. +// API reference: +// https://api.cloudflare.com/#dns-records-for-a-zone-dns-record-details +// GET /zones/:zone_identifier/dns_records/:identifier +func (api *API) DNSRecord(zoneID, recordID string) (DNSRecord, error) { + uri := "/zones/" + zoneID + "/dns_records/" + recordID + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return DNSRecord{}, errors.Wrap(err, errMakeRequestError) + } + var r DNSRecordResponse + err = json.Unmarshal(res, &r) + if err != nil { + return DNSRecord{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// UpdateDNSRecord updates a single DNS record for the given zone & record +// identifiers. +// API reference: +// https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record +// PUT /zones/:zone_identifier/dns_records/:identifier +func (api *API) UpdateDNSRecord(zoneID, recordID string, rr DNSRecord) error { + rec, err := api.DNSRecord(zoneID, recordID) + if err != nil { + return err + } + // Populate the record name from the existing one if the update didn't + // specify it. + if rr.Name == "" { + rr.Name = rec.Name + } + rr.Type = rec.Type + uri := "/zones/" + zoneID + "/dns_records/" + recordID + res, err := api.makeRequest("PUT", uri, rr) + if err != nil { + return errors.Wrap(err, errMakeRequestError) + } + var r DNSRecordResponse + err = json.Unmarshal(res, &r) + if err != nil { + return errors.Wrap(err, errUnmarshalError) + } + return nil +} + +// DeleteDNSRecord deletes a single DNS record for the given zone & record +// identifiers. +// API reference: +// https://api.cloudflare.com/#dns-records-for-a-zone-delete-dns-record +// DELETE /zones/:zone_identifier/dns_records/:identifier +func (api *API) DeleteDNSRecord(zoneID, recordID string) error { + uri := "/zones/" + zoneID + "/dns_records/" + recordID + res, err := api.makeRequest("DELETE", uri, nil) + if err != nil { + return errors.Wrap(err, errMakeRequestError) + } + var r DNSRecordResponse + err = json.Unmarshal(res, &r) + if err != nil { + return errors.Wrap(err, errUnmarshalError) + } + return nil +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/errors.go b/vendor/github.com/cloudflare/cloudflare-go/errors.go new file mode 100644 index 000000000..a909d2170 --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/errors.go @@ -0,0 +1,47 @@ +package cloudflare + +// Error messages +const ( + errEmptyCredentials = "invalid credentials: key & email must not be empty" + errMakeRequestError = "error from makeRequest" + errUnmarshalError = "error unmarshalling the JSON response" +) + +var _ Error = &UserError{} + +// Error represents an error returned from this library. +type Error interface { + error + // Raised when user credentials or configuration is invalid. + User() bool + // Raised when a parsing error (e.g. JSON) occurs. + Parse() bool + // Raised when a network error occurs. + Network() bool + // Contains the most recent error. +} + +// UserError represents a user-generated error. +type UserError struct { + Err error +} + +// User is a user-caused error. +func (e *UserError) User() bool { + return true +} + +// Network error. +func (e *UserError) Network() bool { + return false +} + +// Parse error. +func (e *UserError) Parse() bool { + return true +} + +// Error wraps the underlying error. +func (e *UserError) Error() string { + return e.Err.Error() +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/ips.go b/vendor/github.com/cloudflare/cloudflare-go/ips.go new file mode 100644 index 000000000..8de29defc --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/ips.go @@ -0,0 +1,48 @@ +package cloudflare + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/pkg/errors" +) + +// IPRanges contains lists of IPv4 and IPv6 CIDRs +type IPRanges struct { + IPv4CIDRs []string `json:"ipv4_cidrs"` + IPv6CIDRs []string `json:"ipv6_cidrs"` +} + +// IPsResponse is the API response containing a list of IPs +type IPsResponse struct { + Response + Result IPRanges `json:"result"` +} + +/* +IPs gets a list of Cloudflare's IP ranges + +This does not require logging in to the API. + +API reference: + https://api.cloudflare.com/#cloudflare-ips + GET /client/v4/ips +*/ +func IPs() (IPRanges, error) { + resp, err := http.Get(apiURL + "/ips") + if err != nil { + return IPRanges{}, errors.Wrap(err, "HTTP request failed") + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return IPRanges{}, errors.Wrap(err, "Response body could not be read") + } + var r IPsResponse + err = json.Unmarshal(body, &r) + if err != nil { + return IPRanges{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/keyless.go b/vendor/github.com/cloudflare/cloudflare-go/keyless.go new file mode 100644 index 000000000..ec79f7e0f --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/keyless.go @@ -0,0 +1,57 @@ +package cloudflare + +import "time" + +// KeylessSSL represents Keyless SSL configuration. +type KeylessSSL struct { + ID string `json:"id"` + Name string `json:"name"` + Host string `json:"host"` + Port int `json:"port"` + Status string `json:"success"` + Enabled bool `json:"enabled"` + Permissions []string `json:"permissions"` + CreatedOn time.Time `json:"created_on"` + ModifiedOn time.Time `json:"modifed_on"` +} + +// KeylessSSLResponse represents the response from the Keyless SSL endpoint. +type KeylessSSLResponse struct { + Response + Result []KeylessSSL `json:"result"` +} + +// CreateKeyless creates a new Keyless SSL configuration for the zone. +// API reference: +// https://api.cloudflare.com/#keyless-ssl-for-a-zone-create-a-keyless-ssl-configuration +// POST /zones/:zone_identifier/keyless_certificates +func (api *API) CreateKeyless() { +} + +// ListKeyless lists Keyless SSL configurations for a zone. +// API reference: +// https://api.cloudflare.com/#keyless-ssl-for-a-zone-list-keyless-ssls +// GET /zones/:zone_identifier/keyless_certificates +func (api *API) ListKeyless() { +} + +// Keyless provides the configuration for a given Keyless SSL identifier. +// API reference: +// https://api.cloudflare.com/#keyless-ssl-for-a-zone-keyless-ssl-details +// GET /zones/:zone_identifier/keyless_certificates/:identifier +func (api *API) Keyless() { +} + +// UpdateKeyless updates an existing Keyless SSL configuration. +// API reference: +// https://api.cloudflare.com/#keyless-ssl-for-a-zone-update-keyless-configuration +// PATCH /zones/:zone_identifier/keyless_certificates/:identifier +func (api *API) UpdateKeyless() { +} + +// DeleteKeyless deletes an existing Keyless SSL configuration. +// API reference: +// https://api.cloudflare.com/#keyless-ssl-for-a-zone-delete-keyless-configuration +// DELETE /zones/:zone_identifier/keyless_certificates/:identifier +func (api *API) DeleteKeyless() { +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/options.go b/vendor/github.com/cloudflare/cloudflare-go/options.go new file mode 100644 index 000000000..33ad18c1d --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/options.go @@ -0,0 +1,39 @@ +package cloudflare + +import "net/http" + +// Option is a functional option for configuring the API client. +type Option func(*API) error + +// HTTPClient accepts a custom *http.Client for making API calls. +func HTTPClient(client *http.Client) Option { + return func(api *API) error { + api.httpClient = client + return nil + } +} + +// Headers allows you to set custom HTTP headers when making API calls (e.g. for +// satisfying HTTP proxies, or for debugging). +func Headers(headers http.Header) Option { + return func(api *API) error { + api.headers = headers + return nil + } +} + +// parseOptions parses the supplied options functions and returns a configured +// *API instance. +func (api *API) parseOptions(opts ...Option) error { + // Range over each options function and apply it to our API type to + // configure it. Options functions are applied in order, with any + // conflicting options overriding earlier calls. + for _, option := range opts { + err := option(api) + if err != nil { + return err + } + } + + return nil +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/organizations.go b/vendor/github.com/cloudflare/cloudflare-go/organizations.go new file mode 100644 index 000000000..90e3213e3 --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/organizations.go @@ -0,0 +1,42 @@ +package cloudflare + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +// Organization represents a multi-user organization. +type Organization struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Status string `json:"status,omitempty"` + Permissions []string `json:"permissions,omitempty"` + Roles []string `json:"roles,omitempty"` +} + +// organizationResponse represents the response from the Organization endpoint. +type organizationResponse struct { + Response + Result []Organization `json:"result"` + ResultInfo `json:"result_info"` +} + +// ListOrganizations lists organizations of the logged-in user. +// API reference: +// https://api.cloudflare.com/#user-s-organizations-list-organizations +// GET /user/organizations +func (api *API) ListOrganizations() ([]Organization, ResultInfo, error) { + var r organizationResponse + res, err := api.makeRequest("GET", "/user/organizations", nil) + if err != nil { + return []Organization{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError) + } + + err = json.Unmarshal(res, &r) + if err != nil { + return []Organization{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError) + } + + return r.Result, r.ResultInfo, nil +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/organizations_test.go b/vendor/github.com/cloudflare/cloudflare-go/organizations_test.go new file mode 100644 index 000000000..a3a717dfa --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/organizations_test.go @@ -0,0 +1,66 @@ +package cloudflare + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOrganizations_ListOrganizations(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/user/organizations", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method, "Expected method 'GET', got %s", r.Method) + + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ +"success": true, +"errors": [], +"messages": [], +"result": [ + { + "id": "01a7362d577a6c3019a474fd6f485823", + "name": "Cloudflare, Inc.", + "status": "member", + "permissions": [ + "#zones:read" + ], + "roles": [ + "All Privileges - Super Administrator" + ] + } + ], +"result_info": { + "page": 1, + "per_page": 20, + "count": 1, + "total_count": 2000 + } +}`) + }) + + user, paginator, err := client.ListOrganizations() + + want := []Organization{{ + ID: "01a7362d577a6c3019a474fd6f485823", + Name: "Cloudflare, Inc.", + Status: "member", + Permissions: []string{"#zones:read"}, + Roles: []string{"All Privileges - Super Administrator"}, + }} + + if assert.NoError(t, err) { + assert.Equal(t, user, want) + } + + want_pagination := ResultInfo{ + Page: 1, + PerPage: 20, + Count: 1, + Total: 2000, + } + assert.Equal(t, paginator, want_pagination) +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/pagerules.go b/vendor/github.com/cloudflare/cloudflare-go/pagerules.go new file mode 100644 index 000000000..a382829a2 --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/pagerules.go @@ -0,0 +1,232 @@ +package cloudflare + +import ( + "encoding/json" + "time" + + "github.com/pkg/errors" +) + +/* +PageRuleTarget is the target to evaluate on a request. + +Currently Target must always be "url" and Operator must be "matches". Value +is the URL pattern to match against. +*/ +type PageRuleTarget struct { + Target string `json:"target"` + Constraint struct { + Operator string `json:"operator"` + Value string `json:"value"` + } `json:"constraint"` +} + +/* +PageRuleAction is the action to take when the target is matched. + +Valid IDs are: + + always_online + always_use_https + browser_cache_ttl + browser_check + cache_level + disable_apps + disable_performance + disable_railgun + disable_security + edge_cache_ttl + email_obfuscation + forwarding_url + ip_geolocation + mirage + rocket_loader + security_level + server_side_exclude + smart_errors + ssl + waf +*/ +type PageRuleAction struct { + ID string `json:"id"` + Value interface{} `json:"value"` +} + +// PageRuleActions maps API action IDs to human-readable strings +var PageRuleActions = map[string]string{ + "always_online": "Always Online", // Value of type string + "always_use_https": "Always Use HTTPS", // Value of type interface{} + "browser_cache_ttl": "Browser Cache TTL", // Value of type int + "browser_check": "Browser Integrity Check", // Value of type string + "cache_level": "Cache Level", // Value of type string + "disable_apps": "Disable Apps", // Value of type interface{} + "disable_performance": "Disable Performance", // Value of type interface{} + "disable_railgun": "Disable Railgun", // Value of type string + "disable_security": "Disable Security", // Value of type interface{} + "edge_cache_ttl": "Edge Cache TTL", // Value of type int + "email_obfuscation": "Email Obfuscation", // Value of type string + "forwarding_url": "Forwarding URL", // Value of type map[string]interface + "ip_geolocation": "IP Geolocation Header", // Value of type string + "mirage": "Mirage", // Value of type string + "rocket_loader": "Rocker Loader", // Value of type string + "security_level": "Security Level", // Value of type string + "server_side_exclude": "Server Side Excludes", // Value of type string + "smart_errors": "Smart Errors", // Value of type string + "ssl": "SSL", // Value of type string + "waf": "Web Application Firewall", // Value of type string +} + +// PageRule describes a Page Rule. +type PageRule struct { + ID string `json:"id,omitempty"` + Targets []PageRuleTarget `json:"targets"` + Actions []PageRuleAction `json:"actions"` + Priority int `json:"priority"` + Status string `json:"status"` // can be: active, paused + ModifiedOn time.Time `json:"modified_on,omitempty"` + CreatedOn time.Time `json:"created_on,omitempty"` +} + +// PageRuleDetailResponse is the API response, containing a single PageRule. +type PageRuleDetailResponse struct { + Success bool `json:"success"` + Errors []string `json:"errors"` + Messages []string `json:"messages"` + Result PageRule `json:"result"` +} + +// PageRulesResponse is the API response, containing an array of PageRules. +type PageRulesResponse struct { + Success bool `json:"success"` + Errors []string `json:"errors"` + Messages []string `json:"messages"` + Result []PageRule `json:"result"` +} + +/* +CreatePageRule creates a new Page Rule for a zone. + +API reference: + https://api.cloudflare.com/#page-rules-for-a-zone-create-a-page-rule + POST /zones/:zone_identifier/pagerules +*/ +func (api *API) CreatePageRule(zoneID string, rule PageRule) error { + uri := "/zones/" + zoneID + "/pagerules" + res, err := api.makeRequest("POST", uri, rule) + if err != nil { + return errors.Wrap(err, errMakeRequestError) + } + var r PageRuleDetailResponse + err = json.Unmarshal(res, &r) + if err != nil { + return errors.Wrap(err, errUnmarshalError) + } + return nil +} + +/* +ListPageRules returns all Page Rules for a zone. + +API reference: + https://api.cloudflare.com/#page-rules-for-a-zone-list-page-rules + GET /zones/:zone_identifier/pagerules +*/ +func (api *API) ListPageRules(zoneID string) ([]PageRule, error) { + uri := "/zones/" + zoneID + "/pagerules" + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return []PageRule{}, errors.Wrap(err, errMakeRequestError) + } + var r PageRulesResponse + err = json.Unmarshal(res, &r) + if err != nil { + return []PageRule{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +/* +PageRule fetches detail about one Page Rule for a zone. + +API reference: + https://api.cloudflare.com/#page-rules-for-a-zone-page-rule-details + GET /zones/:zone_identifier/pagerules/:identifier +*/ +func (api *API) PageRule(zoneID, ruleID string) (PageRule, error) { + uri := "/zones/" + zoneID + "/pagerules/" + ruleID + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return PageRule{}, errors.Wrap(err, errMakeRequestError) + } + var r PageRuleDetailResponse + err = json.Unmarshal(res, &r) + if err != nil { + return PageRule{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +/* +ChangePageRule lets change individual settings for a Page Rule. This is in +contrast to UpdatePageRule which replaces the entire Page Rule. + +API reference: + https://api.cloudflare.com/#page-rules-for-a-zone-change-a-page-rule + PATCH /zones/:zone_identifier/pagerules/:identifier +*/ +func (api *API) ChangePageRule(zoneID, ruleID string, rule PageRule) error { + uri := "/zones/" + zoneID + "/pagerules/" + ruleID + res, err := api.makeRequest("PATCH", uri, rule) + if err != nil { + return errors.Wrap(err, errMakeRequestError) + } + var r PageRuleDetailResponse + err = json.Unmarshal(res, &r) + if err != nil { + return errors.Wrap(err, errUnmarshalError) + } + return nil +} + +/* +UpdatePageRule lets you replace a Page Rule. This is in contrast to +ChangePageRule which lets you change individual settings. + +API reference: + https://api.cloudflare.com/#page-rules-for-a-zone-update-a-page-rule + PUT /zones/:zone_identifier/pagerules/:identifier +*/ +func (api *API) UpdatePageRule(zoneID, ruleID string, rule PageRule) error { + uri := "/zones/" + zoneID + "/pagerules/" + ruleID + res, err := api.makeRequest("PUT", uri, nil) + if err != nil { + return errors.Wrap(err, errMakeRequestError) + } + var r PageRuleDetailResponse + err = json.Unmarshal(res, &r) + if err != nil { + return errors.Wrap(err, errUnmarshalError) + } + return nil +} + +/* +DeletePageRule deletes a Page Rule for a zone. + +API reference: + https://api.cloudflare.com/#page-rules-for-a-zone-delete-a-page-rule + DELETE /zones/:zone_identifier/pagerules/:identifier +*/ +func (api *API) DeletePageRule(zoneID, ruleID string) error { + uri := "/zones/" + zoneID + "/pagerules/" + ruleID + res, err := api.makeRequest("DELETE", uri, nil) + if err != nil { + return errors.Wrap(err, errMakeRequestError) + } + var r PageRuleDetailResponse + err = json.Unmarshal(res, &r) + if err != nil { + return errors.Wrap(err, errUnmarshalError) + } + return nil +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/railgun.go b/vendor/github.com/cloudflare/cloudflare-go/railgun.go new file mode 100644 index 000000000..276a08686 --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/railgun.go @@ -0,0 +1,311 @@ +package cloudflare + +import ( + "encoding/json" + "net/url" + "time" + + "github.com/pkg/errors" +) + +// Railgun represents a Railgun's properties. +type Railgun struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Enabled bool `json:"enabled"` + ZonesConnected int `json:"zones_connected"` + Build string `json:"build"` + Version string `json:"version"` + Revision string `json:"revision"` + ActivationKey string `json:"activation_key"` + ActivatedOn time.Time `json:"activated_on"` + CreatedOn time.Time `json:"created_on"` + ModifiedOn time.Time `json:"modified_on"` + UpgradeInfo struct { + LatestVersion string `json:"latest_version"` + DownloadLink string `json:"download_link"` + } `json:"upgrade_info"` +} + +// RailgunListOptions represents the parameters used to list railguns. +type RailgunListOptions struct { + Direction string +} + +// railgunResponse represents the response from the Create Railgun and the Railgun Details endpoints. +type railgunResponse struct { + Response + Result Railgun `json:"result"` +} + +// railgunsResponse represents the response from the List Railguns endpoint. +type railgunsResponse struct { + Response + Result []Railgun `json:"result"` +} + +// CreateRailgun creates a new Railgun. +// API reference: +// https://api.cloudflare.com/#railgun-create-railgun +// POST /railguns +func (api *API) CreateRailgun(name string) (Railgun, error) { + uri := "/railguns" + params := struct { + Name string `json:"name"` + }{ + Name: name, + } + res, err := api.makeRequest("POST", uri, params) + if err != nil { + return Railgun{}, errors.Wrap(err, errMakeRequestError) + } + var r railgunResponse + if err := json.Unmarshal(res, &r); err != nil { + return Railgun{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// ListRailguns lists Railguns connected to an account. +// API reference: +// https://api.cloudflare.com/#railgun-list-railguns +// GET /railguns +func (api *API) ListRailguns(options RailgunListOptions) ([]Railgun, error) { + v := url.Values{} + if options.Direction != "" { + v.Set("direction", options.Direction) + } + uri := "/railguns" + "?" + v.Encode() + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return nil, errors.Wrap(err, errMakeRequestError) + } + var r railgunsResponse + if err := json.Unmarshal(res, &r); err != nil { + return nil, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// RailgunDetails returns the details for a Railgun. +// API reference: +// https://api.cloudflare.com/#railgun-railgun-details +// GET /railguns/:identifier +func (api *API) RailgunDetails(railgunID string) (Railgun, error) { + uri := "/railguns/" + railgunID + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return Railgun{}, errors.Wrap(err, errMakeRequestError) + } + var r railgunResponse + if err := json.Unmarshal(res, &r); err != nil { + return Railgun{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// RailgunZones returns the zones that are currently using a Railgun. +// API reference: +// https://api.cloudflare.com/#railgun-get-zones-connected-to-a-railgun +// GET /railguns/:identifier/zones +func (api *API) RailgunZones(railgunID string) ([]Zone, error) { + uri := "/railguns/" + railgunID + "/zones" + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return nil, errors.Wrap(err, errMakeRequestError) + } + var r ZonesResponse + if err := json.Unmarshal(res, &r); err != nil { + return nil, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// enableRailgun enables (true) or disables (false) a Railgun for all zones connected to it. +// API reference: +// https://api.cloudflare.com/#railgun-enable-or-disable-a-railgun +// PATCH /railguns/:identifier +func (api *API) enableRailgun(railgunID string, enable bool) (Railgun, error) { + uri := "/railguns/" + railgunID + params := struct { + Enabled bool `json:"enabled"` + }{ + Enabled: enable, + } + res, err := api.makeRequest("PATCH", uri, params) + if err != nil { + return Railgun{}, errors.Wrap(err, errMakeRequestError) + } + var r railgunResponse + if err := json.Unmarshal(res, &r); err != nil { + return Railgun{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// EnableRailgun enables a Railgun for all zones connected to it. +// API reference: +// https://api.cloudflare.com/#railgun-enable-or-disable-a-railgun +// PATCH /railguns/:identifier +func (api *API) EnableRailgun(railgunID string) (Railgun, error) { + return api.enableRailgun(railgunID, true) +} + +// DisableRailgun enables a Railgun for all zones connected to it. +// API reference: +// https://api.cloudflare.com/#railgun-enable-or-disable-a-railgun +// PATCH /railguns/:identifier +func (api *API) DisableRailgun(railgunID string) (Railgun, error) { + return api.enableRailgun(railgunID, false) +} + +// DeleteRailgun disables and deletes a Railgun. +// API reference: +// https://api.cloudflare.com/#railgun-delete-railgun +// DELETE /railguns/:identifier +func (api *API) DeleteRailgun(railgunID string) error { + uri := "/railguns/" + railgunID + if _, err := api.makeRequest("DELETE", uri, nil); err != nil { + return errors.Wrap(err, errMakeRequestError) + } + return nil +} + +// ZoneRailgun represents the status of a Railgun on a zone. +type ZoneRailgun struct { + ID string `json:"id"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + Connected bool `json:"connected"` +} + +// zoneRailgunResponse represents the response from the Zone Railgun Details endpoint. +type zoneRailgunResponse struct { + Response + Result ZoneRailgun `json:"result"` +} + +// zoneRailgunsResponse represents the response from the Zone Railgun endpoint. +type zoneRailgunsResponse struct { + Response + Result []ZoneRailgun `json:"result"` +} + +// RailgunDiagnosis represents the test results from testing railgun connections +// to a zone. +type RailgunDiagnosis struct { + Method string `json:"method"` + HostName string `json:"host_name"` + HTTPStatus int `json:"http_status"` + Railgun string `json:"railgun"` + URL string `json:"url"` + ResponseStatus string `json:"response_status"` + Protocol string `json:"protocol"` + ElapsedTime string `json:"elapsed_time"` + BodySize string `json:"body_size"` + BodyHash string `json:"body_hash"` + MissingHeaders string `json:"missing_headers"` + ConnectionClose bool `json:"connection_close"` + Cloudflare string `json:"cloudflare"` + CFRay string `json:"cf-ray"` + // NOTE: Cloudflare's online API documentation does not yet have definitions + // for the following fields. See: https://api.cloudflare.com/#railgun-connections-for-a-zone-test-railgun-connection/ + CFWANError string `json:"cf-wan-error"` + CFCacheStatus string `json:"cf-cache-status"` +} + +// railgunDiagnosisResponse represents the response from the Test Railgun Connection enpoint. +type railgunDiagnosisResponse struct { + Response + Result RailgunDiagnosis `json:"result"` +} + +// ZoneRailguns returns the available Railguns for a zone. +// API reference: +// https://api.cloudflare.com/#railguns-for-a-zone-get-available-railguns +// GET /zones/:zone_identifier/railguns +func (api *API) ZoneRailguns(zoneID string) ([]ZoneRailgun, error) { + uri := "/zones/" + zoneID + "/railguns" + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return nil, errors.Wrap(err, errMakeRequestError) + } + var r zoneRailgunsResponse + if err := json.Unmarshal(res, &r); err != nil { + return nil, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// ZoneRailgunDetails returns the configuration for a given Railgun. +// API reference: +// https://api.cloudflare.com/#railguns-for-a-zone-get-railgun-details +// GET /zones/:zone_identifier/railguns/:identifier +func (api *API) ZoneRailgunDetails(zoneID, railgunID string) (ZoneRailgun, error) { + uri := "/zones/" + zoneID + "/railguns/" + railgunID + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return ZoneRailgun{}, errors.Wrap(err, errMakeRequestError) + } + var r zoneRailgunResponse + if err := json.Unmarshal(res, &r); err != nil { + return ZoneRailgun{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// TestRailgunConnection tests a Railgun connection for a given zone. +// API reference: +// https://api.cloudflare.com/#railgun-connections-for-a-zone-test-railgun-connection +// GET /zones/:zone_identifier/railguns/:identifier/diagnose +func (api *API) TestRailgunConnection(zoneID, railgunID string) (RailgunDiagnosis, error) { + uri := "/zones/" + zoneID + "/railguns/" + railgunID + "/diagnose" + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return RailgunDiagnosis{}, errors.Wrap(err, errMakeRequestError) + } + var r railgunDiagnosisResponse + if err := json.Unmarshal(res, &r); err != nil { + return RailgunDiagnosis{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// connectZoneRailgun connects (true) or disconnects (false) a Railgun for a given zone. +// API reference: +// https://api.cloudflare.com/#railguns-for-a-zone-connect-or-disconnect-a-railgun +// PATCH /zones/:zone_identifier/railguns/:identifier +func (api *API) connectZoneRailgun(zoneID, railgunID string, connect bool) (ZoneRailgun, error) { + uri := "/zones/" + zoneID + "/railguns/" + railgunID + params := struct { + Connected bool `json:"connected"` + }{ + Connected: connect, + } + res, err := api.makeRequest("PATCH", uri, params) + if err != nil { + return ZoneRailgun{}, errors.Wrap(err, errMakeRequestError) + } + var r zoneRailgunResponse + if err := json.Unmarshal(res, &r); err != nil { + return ZoneRailgun{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// ConnectZoneRailgun connects a Railgun for a given zone. +// API reference: +// https://api.cloudflare.com/#railguns-for-a-zone-connect-or-disconnect-a-railgun +// PATCH /zones/:zone_identifier/railguns/:identifier +func (api *API) ConnectZoneRailgun(zoneID, railgunID string) (ZoneRailgun, error) { + return api.connectZoneRailgun(zoneID, railgunID, true) +} + +// DisconnectZoneRailgun disconnects a Railgun for a given zone. +// API reference: +// https://api.cloudflare.com/#railguns-for-a-zone-connect-or-disconnect-a-railgun +// PATCH /zones/:zone_identifier/railguns/:identifier +func (api *API) DisconnectZoneRailgun(zoneID, railgunID string) (ZoneRailgun, error) { + return api.connectZoneRailgun(zoneID, railgunID, false) +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/railgun_test.go b/vendor/github.com/cloudflare/cloudflare-go/railgun_test.go new file mode 100644 index 000000000..b9c8f498d --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/railgun_test.go @@ -0,0 +1,621 @@ +package cloudflare + +import ( + "fmt" + "io/ioutil" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestCreateRailgun(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "POST", "Expected method 'POST', got %s", r.Method) + w.Header().Set("content-type", "application/json") + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if assert.NoError(t, err) { + assert.JSONEq(t, `{"name":"My Railgun"}`, string(b)) + } + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "e928d310693a83094309acf9ead50448", + "name": "My Railgun", + "status": "active", + "enabled": true, + "zones_connected": 2, + "build": "b1234", + "version": "2.1", + "revision": "123", + "activation_key": "e4edc00281cb56ebac22c81be9bac8f3", + "activated_on": "2014-01-02T02:20:00Z", + "created_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z" + } + }`) + } + + mux.HandleFunc("/railguns", handler) + activatedOn, _ := time.Parse(time.RFC3339, "2014-01-02T02:20:00Z") + createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + want := Railgun{ + ID: "e928d310693a83094309acf9ead50448", + Name: "My Railgun", + Status: "active", + Enabled: true, + ZonesConnected: 2, + Build: "b1234", + Version: "2.1", + Revision: "123", + ActivationKey: "e4edc00281cb56ebac22c81be9bac8f3", + ActivatedOn: activatedOn, + CreatedOn: createdOn, + ModifiedOn: modifiedOn, + } + + actual, err := client.CreateRailgun("My Railgun") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestListRailguns(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "id": "e928d310693a83094309acf9ead50448", + "name": "My Railgun", + "status": "active", + "enabled": true, + "zones_connected": 2, + "build": "b1234", + "version": "2.1", + "revision": "123", + "activation_key": "e4edc00281cb56ebac22c81be9bac8f3", + "activated_on": "2014-01-02T02:20:00Z", + "created_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z" + } + ], + "result_info": { + "page": 1, + "per_page": 20, + "count": 1, + "total_count": 2000 + } + }`) + } + + mux.HandleFunc("/railguns", handler) + activatedOn, _ := time.Parse(time.RFC3339, "2014-01-02T02:20:00Z") + createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + want := []Railgun{ + { + ID: "e928d310693a83094309acf9ead50448", + Name: "My Railgun", + Status: "active", + Enabled: true, + ZonesConnected: 2, + Build: "b1234", + Version: "2.1", + Revision: "123", + ActivationKey: "e4edc00281cb56ebac22c81be9bac8f3", + ActivatedOn: activatedOn, + CreatedOn: createdOn, + ModifiedOn: modifiedOn, + }, + } + + actual, err := client.ListRailguns(RailgunListOptions{Direction: "desc"}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestRailgunDetails(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "e928d310693a83094309acf9ead50448", + "name": "My Railgun", + "status": "active", + "enabled": true, + "zones_connected": 2, + "build": "b1234", + "version": "2.1", + "revision": "123", + "activation_key": "e4edc00281cb56ebac22c81be9bac8f3", + "activated_on": "2014-01-02T02:20:00Z", + "created_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z" + } + }`) + } + + mux.HandleFunc("/railguns/e928d310693a83094309acf9ead50448", handler) + activatedOn, _ := time.Parse(time.RFC3339, "2014-01-02T02:20:00Z") + createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + want := Railgun{ + ID: "e928d310693a83094309acf9ead50448", + Name: "My Railgun", + Status: "active", + Enabled: true, + ZonesConnected: 2, + Build: "b1234", + Version: "2.1", + Revision: "123", + ActivationKey: "e4edc00281cb56ebac22c81be9bac8f3", + ActivatedOn: activatedOn, + CreatedOn: createdOn, + ModifiedOn: modifiedOn, + } + + actual, err := client.RailgunDetails("e928d310693a83094309acf9ead50448") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.RailgunDetails("bar") + assert.Error(t, err) +} + +func TestRailgunZones(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "id": "023e105f4ecef8ad9ca31a8372d0c353", + "name": "example.com", + "development_mode": 7200, + "original_name_servers": [ + "ns1.originaldnshost.com", + "ns2.originaldnshost.com" + ], + "original_registrar": "GoDaddy", + "original_dnshost": "NameCheap", + "created_on": "2014-01-01T05:20:00.12345Z", + "modified_on": "2014-01-01T05:20:00.12345Z" + } + ], + "result_info": { + "page": 1, + "per_page": 20, + "count": 1, + "total_count": 2000 + } + }`) + } + + mux.HandleFunc("/railguns/e928d310693a83094309acf9ead50448/zones", handler) + createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") + want := []Zone{ + { + ID: "023e105f4ecef8ad9ca31a8372d0c353", + Name: "example.com", + DevMode: 7200, + OriginalNS: []string{"ns1.originaldnshost.com", "ns2.originaldnshost.com"}, + OriginalRegistrar: "GoDaddy", + OriginalDNSHost: "NameCheap", + CreatedOn: createdOn, + ModifiedOn: modifiedOn, + }, + } + + actual, err := client.RailgunZones("e928d310693a83094309acf9ead50448") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.RailgunZones("bar") + assert.Error(t, err) +} + +func TestEnableRailgun(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "PATCH", "Expected method 'PATCH', got %s", r.Method) + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if assert.NoError(t, err) { + assert.JSONEq(t, `{"enabled":true}`, string(b)) + } + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "e928d310693a83094309acf9ead50448", + "name": "My Railgun", + "status": "active", + "enabled": true, + "zones_connected": 2, + "build": "b1234", + "version": "2.1", + "revision": "123", + "activation_key": "e4edc00281cb56ebac22c81be9bac8f3", + "activated_on": "2014-01-02T02:20:00Z", + "created_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z" + } + }`) + } + + mux.HandleFunc("/railguns/e928d310693a83094309acf9ead50448", handler) + activatedOn, _ := time.Parse(time.RFC3339, "2014-01-02T02:20:00Z") + createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + want := Railgun{ + ID: "e928d310693a83094309acf9ead50448", + Name: "My Railgun", + Status: "active", + Enabled: true, + ZonesConnected: 2, + Build: "b1234", + Version: "2.1", + Revision: "123", + ActivationKey: "e4edc00281cb56ebac22c81be9bac8f3", + ActivatedOn: activatedOn, + CreatedOn: createdOn, + ModifiedOn: modifiedOn, + } + + actual, err := client.EnableRailgun("e928d310693a83094309acf9ead50448") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.EnableRailgun("bar") + assert.Error(t, err) +} + +func TestDisbleRailgun(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "PATCH", "Expected method 'PATCH', got %s", r.Method) + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if assert.NoError(t, err) { + assert.JSONEq(t, `{"enabled":false}`, string(b)) + } + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "e928d310693a83094309acf9ead50448", + "name": "My Railgun", + "status": "active", + "enabled": false, + "zones_connected": 2, + "build": "b1234", + "version": "2.1", + "revision": "123", + "activation_key": "e4edc00281cb56ebac22c81be9bac8f3", + "activated_on": "2014-01-02T02:20:00Z", + "created_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z" + } + }`) + } + + mux.HandleFunc("/railguns/e928d310693a83094309acf9ead50448", handler) + activatedOn, _ := time.Parse(time.RFC3339, "2014-01-02T02:20:00Z") + createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + want := Railgun{ + ID: "e928d310693a83094309acf9ead50448", + Name: "My Railgun", + Status: "active", + Enabled: false, + ZonesConnected: 2, + Build: "b1234", + Version: "2.1", + Revision: "123", + ActivationKey: "e4edc00281cb56ebac22c81be9bac8f3", + ActivatedOn: activatedOn, + CreatedOn: createdOn, + ModifiedOn: modifiedOn, + } + + actual, err := client.DisableRailgun("e928d310693a83094309acf9ead50448") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.DisableRailgun("bar") + assert.Error(t, err) +} + +func TestDeleteRailgun(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "DELETE", "Expected method 'DELETE', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "e928d310693a83094309acf9ead50448" + } + }`) + } + + mux.HandleFunc("/railguns/e928d310693a83094309acf9ead50448", handler) + assert.NoError(t, client.DeleteRailgun("e928d310693a83094309acf9ead50448")) + assert.Error(t, client.DeleteRailgun("bar")) +} + +func TestZoneRailguns(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "id": "e928d310693a83094309acf9ead50448", + "name": "My Railgun", + "enabled": true, + "connected": true + } + ], + "result_info": { + "page": 1, + "per_page": 20, + "count": 1, + "total_count": 2000 + } + }`) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/railguns", handler) + want := []ZoneRailgun{ + { + ID: "e928d310693a83094309acf9ead50448", + Name: "My Railgun", + Enabled: true, + Connected: true, + }, + } + + actual, err := client.ZoneRailguns("023e105f4ecef8ad9ca31a8372d0c353") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.ZoneRailguns("bar") + assert.Error(t, err) +} + +func TestZoneRailgunDetails(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "e928d310693a83094309acf9ead50448", + "name": "My Railgun", + "enabled": true, + "connected": true + } + }`) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/railguns/e928d310693a83094309acf9ead50448", handler) + want := ZoneRailgun{ + ID: "e928d310693a83094309acf9ead50448", + Name: "My Railgun", + Enabled: true, + Connected: true, + } + + actual, err := client.ZoneRailgunDetails("023e105f4ecef8ad9ca31a8372d0c353", "e928d310693a83094309acf9ead50448") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.ZoneRailgunDetails("bar", "baz") + assert.Error(t, err) +} + +func TestTestRailgunConnection(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "method": "GET", + "host_name": "www.example.com", + "http_status": 200, + "railgun": "on", + "url": "https://www.cloudflare.com", + "response_status": "200 OK", + "protocol": "HTTP/1.1", + "elapsed_time": "0.239013s", + "body_size": "63910 bytes", + "body_hash": "be27f2429421e12f200cab1da43ba301bdc70e1d", + "missing_headers": "No Content-Length or Transfer-Encoding", + "connection_close": false, + "cloudflare": "on", + "cf-ray": "1ddd7570575207d9-LAX", + "cf-wan-error": null, + "cf-cache-status": null + } + }`) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/railguns/e928d310693a83094309acf9ead50448/diagnose", handler) + want := RailgunDiagnosis{ + Method: "GET", + HostName: "www.example.com", + HTTPStatus: 200, + Railgun: "on", + URL: "https://www.cloudflare.com", + ResponseStatus: "200 OK", + Protocol: "HTTP/1.1", + ElapsedTime: "0.239013s", + BodySize: "63910 bytes", + BodyHash: "be27f2429421e12f200cab1da43ba301bdc70e1d", + MissingHeaders: "No Content-Length or Transfer-Encoding", + ConnectionClose: false, + Cloudflare: "on", + CFRay: "1ddd7570575207d9-LAX", + CFWANError: "", + CFCacheStatus: "", + } + + actual, err := client.TestRailgunConnection("023e105f4ecef8ad9ca31a8372d0c353", "e928d310693a83094309acf9ead50448") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.TestRailgunConnection("bar", "baz") + assert.Error(t, err) +} + +func TestConnectRailgun(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "PATCH", "Expected method 'PATCH', got %s", r.Method) + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if assert.NoError(t, err) { + assert.JSONEq(t, `{"connected":true}`, string(b)) + } + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "e928d310693a83094309acf9ead50448", + "name": "My Railgun", + "enabled": true, + "connected": true + } + }`) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/railguns/e928d310693a83094309acf9ead50448", handler) + want := ZoneRailgun{ + ID: "e928d310693a83094309acf9ead50448", + Name: "My Railgun", + Enabled: true, + Connected: true, + } + + actual, err := client.ConnectZoneRailgun("023e105f4ecef8ad9ca31a8372d0c353", "e928d310693a83094309acf9ead50448") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.ConnectZoneRailgun("bar", "baz") + assert.Error(t, err) +} + +func TestDisconnectRailgun(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "PATCH", "Expected method 'PATCH', got %s", r.Method) + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if assert.NoError(t, err) { + assert.JSONEq(t, `{"connected":false}`, string(b)) + } + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "e928d310693a83094309acf9ead50448", + "name": "My Railgun", + "enabled": true, + "connected": false + } + }`) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/railguns/e928d310693a83094309acf9ead50448", handler) + want := ZoneRailgun{ + ID: "e928d310693a83094309acf9ead50448", + Name: "My Railgun", + Enabled: true, + Connected: false, + } + + actual, err := client.DisconnectZoneRailgun("023e105f4ecef8ad9ca31a8372d0c353", "e928d310693a83094309acf9ead50448") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.DisconnectZoneRailgun("bar", "baz") + assert.Error(t, err) +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/ssl.go b/vendor/github.com/cloudflare/cloudflare-go/ssl.go new file mode 100644 index 000000000..d352fad40 --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/ssl.go @@ -0,0 +1,154 @@ +package cloudflare + +import ( + "encoding/json" + "time" + + "github.com/pkg/errors" +) + +// ZoneCustomSSL represents custom SSL certificate metadata. +type ZoneCustomSSL struct { + ID string `json:"id"` + Hosts []string `json:"hosts"` + Issuer string `json:"issuer"` + Signature string `json:"signature"` + Status string `json:"status"` + BundleMethod string `json:"bundle_method"` + ZoneID string `json:"zone_id"` + UploadedOn time.Time `json:"uploaded_on"` + ModifiedOn time.Time `json:"modified_on"` + ExpiresOn time.Time `json:"expires_on"` + Priority int `json:"priority"` + KeylessServer KeylessSSL `json:"keyless_server"` +} + +// zoneCustomSSLResponse represents the response from the zone SSL details endpoint. +type zoneCustomSSLResponse struct { + Response + Result ZoneCustomSSL `json:"result"` +} + +// zoneCustomSSLsResponse represents the response from the zone SSL list endpoint. +type zoneCustomSSLsResponse struct { + Response + Result []ZoneCustomSSL `json:"result"` +} + +// ZoneCustomSSLOptions represents the parameters to create or update an existing +// custom SSL configuration. +type ZoneCustomSSLOptions struct { + Certificate string `json:"certificate"` + PrivateKey string `json:"private_key"` + BundleMethod string `json:"bundle_method,omitempty"` +} + +// ZoneCustomSSLPriority represents a certificate's ID and priority. It is a +// subset of ZoneCustomSSL used for patch requests. +type ZoneCustomSSLPriority struct { + ID string `json:"ID"` + Priority int `json:"priority"` +} + +// CreateSSL allows you to add a custom SSL certificate to the given zone. +// API reference: +// https://api.cloudflare.com/#custom-ssl-for-a-zone-create-ssl-configuration +// POST /zones/:zone_identifier/custom_certificates +func (api *API) CreateSSL(zoneID string, options ZoneCustomSSLOptions) (ZoneCustomSSL, error) { + uri := "/zones/" + zoneID + "/custom_certificates" + res, err := api.makeRequest("POST", uri, options) + if err != nil { + return ZoneCustomSSL{}, errors.Wrap(err, errMakeRequestError) + } + var r zoneCustomSSLResponse + if err := json.Unmarshal(res, &r); err != nil { + return ZoneCustomSSL{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// ListSSL lists the custom certificates for the given zone. +// API reference: +// https://api.cloudflare.com/#custom-ssl-for-a-zone-list-ssl-configurations +// GET /zones/:zone_identifier/custom_certificates +func (api *API) ListSSL(zoneID string) ([]ZoneCustomSSL, error) { + uri := "/zones/" + zoneID + "/custom_certificates" + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return nil, errors.Wrap(err, errMakeRequestError) + } + var r zoneCustomSSLsResponse + if err := json.Unmarshal(res, &r); err != nil { + return nil, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// SSLDetails returns the configuration details for a custom SSL certificate. +// API reference: +// https://api.cloudflare.com/#custom-ssl-for-a-zone-ssl-configuration-details +// GET /zones/:zone_identifier/custom_certificates/:identifier +func (api *API) SSLDetails(zoneID, certificateID string) (ZoneCustomSSL, error) { + uri := "/zones/" + zoneID + "/custom_certificates/" + certificateID + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return ZoneCustomSSL{}, errors.Wrap(err, errMakeRequestError) + } + var r zoneCustomSSLResponse + if err := json.Unmarshal(res, &r); err != nil { + return ZoneCustomSSL{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// UpdateSSL updates (replaces) a custom SSL certificate. +// API reference: +// https://api.cloudflare.com/#custom-ssl-for-a-zone-update-ssl-configuration +// PATCH /zones/:zone_identifier/custom_certificates/:identifier +func (api *API) UpdateSSL(zoneID, certificateID string, options ZoneCustomSSLOptions) (ZoneCustomSSL, error) { + uri := "/zones/" + zoneID + "/custom_certificates/" + certificateID + res, err := api.makeRequest("PATCH", uri, options) + if err != nil { + return ZoneCustomSSL{}, errors.Wrap(err, errMakeRequestError) + } + var r zoneCustomSSLResponse + if err := json.Unmarshal(res, &r); err != nil { + return ZoneCustomSSL{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// ReprioritizeSSL allows you to change the priority (which is served for a given +// request) of custom SSL certificates associated with the given zone. +// API reference: +// https://api.cloudflare.com/#custom-ssl-for-a-zone-re-prioritize-ssl-certificates +// PUT /zones/:zone_identifier/custom_certificates/prioritize +func (api *API) ReprioritizeSSL(zoneID string, p []ZoneCustomSSLPriority) ([]ZoneCustomSSL, error) { + uri := "/zones/" + zoneID + "/custom_certificates/prioritize" + params := struct { + Certificates []ZoneCustomSSLPriority `json:"certificates"` + }{ + Certificates: p, + } + res, err := api.makeRequest("PUT", uri, params) + if err != nil { + return nil, errors.Wrap(err, errMakeRequestError) + } + var r zoneCustomSSLsResponse + if err := json.Unmarshal(res, &r); err != nil { + return nil, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// DeleteSSL deletes a custom SSL certificate from the given zone. +// API reference: +// https://api.cloudflare.com/#custom-ssl-for-a-zone-delete-an-ssl-certificate +// DELETE /zones/:zone_identifier/custom_certificates/:identifier +func (api *API) DeleteSSL(zoneID, certificateID string) error { + uri := "/zones/" + zoneID + "/custom_certificates/" + certificateID + if _, err := api.makeRequest("DELETE", uri, nil); err != nil { + return errors.Wrap(err, errMakeRequestError) + } + return nil +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/ssl_test.go b/vendor/github.com/cloudflare/cloudflare-go/ssl_test.go new file mode 100644 index 000000000..ba77c6cea --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/ssl_test.go @@ -0,0 +1,377 @@ +package cloudflare + +import ( + "fmt" + "io/ioutil" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestCreateSSL(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method, "Expected method 'POST', got %s", r.Method) + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if assert.NoError(t, err) { + assert.JSONEq(t, `{"certificate":"-----BEGIN CERTIFICATE----- MIIDtTCCAp2gAwIBAgIJAM15n7fdxhRtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX aWRnaXRzIFB0eSBMdGQwHhcNMTQwMzExMTkyMTU5WhcNMTQwNDEwMTkyMTU5WjBF MQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB CgKCAQEAvq3sKsHpeduJHimOK+fvQdKsI8z8A05MZyyLp2/R/GE8FjNv+hkVY1WQ LIyTNNQH7CJecE1nbTfo8Y56S7x/rhxC6/DJ8MIulapFPnorq46KU6yRxiM0MQ3N nTJHlHA2ozZta6YBBfVfhHWl1F0IfNbXCLKvGwWWMbCx43OfW6KTkbRnE6gFWKuO fSO5h2u5TaWVuSIzBvYs7Vza6m+gtYAvKAJV2nSZ+eSEFPDo29corOy8+huEOUL8 5FAw4BFPsr1TlrlGPFitduQUHGrSL7skk1ESGza0to3bOtrodKei2s9bk5MXm7lZ qI+WZJX4Zu9+mzZhc9pCVi8r/qlXuQIDAQABo4GnMIGkMB0GA1UdDgQWBBRvavf+ sWM4IwKiH9X9w1vl6nUVRDB1BgNVHSMEbjBsgBRvavf+sWM4IwKiH9X9w1vl6nUV RKFJpEcwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAM15n7fdxhRtMAwGA1UdEwQF MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBABY2ZzBaW0dMsAAT7tPJzrVWVzQx6KU4 UEBLudIlWPlkAwTnINCWR/8eNjCCmGA4heUdHmazdpPa8RzwOmc0NT1NQqzSyktt vTqb4iHD7+8f9MqJ9/FssCfTtqr/Qst/hGH4Wmdf1EJ/6FqYAAb5iRlPgshFZxU8 uXtA8hWn6fK6eISD9HBdcAFToUvKNZ1BIDPvh9f95Ine8ar6yGd56TUNrHR8eHBs ESxz5ddVR/oWRysNJ+aGAyYqHS8S/ttmC7r4XCAHqXptkHPCGRqkAhsterYhd4I8 /cBzejUobNCjjHFbtkAL/SjxZOLW+pNkZwfeYdM8iPkD54Uua1v2tdw= -----END CERTIFICATE-----","private_key":"-----BEGIN RSA PRIVATE KEY-----MIIEowIBAAKCAQEAl 1cSc0vfcJLI4ZdWjiZZqy86Eof4czCwilyjXdvHqbdgDjz9H6K/0FX78EzVdfyExESptPCDl5YYjvcZyAWlgNfYEpFpGeoh/pTFW3hlyKImh4EgBXbDrR251J Ew2Nf56X3duibI6X20gKZA6cvdmWeKh MOOXuh1bSPU3dkb4YOF/fng5iGrx0q3txdMQXTPMZ1uXHFcBH7idgViYesXUBhdll3GP1N Y8laq0yrqh 8HMsZK m27MebqonbNmjOqE218lVEvjCdRO6xvNXrO6vNJBoGn2eGwZ8BVd0mTA3Tj43/2cmxQFY9FLq56cCXqYI1fbRRib ZLrjSNkwIDAQABAoIBABfAjjsjjxc0NxcYvKOMUb9Rpj8Sx6U/o/tDC5u XmsGX37aaJmC5yw9BQiAxgvXtQryEl5uoNoqOdsxzKV6yM0vPcwKEJVBd4G6yx6AjVJZnc2qf72erR7BbA2CQh scMDRBKE041HhgTBRNP6roim0SOgYP5JZIrGAQXNIkyE0fZc5gZNUt388ne/mjWM6Xi08BDGurLC68nsdt7Nd UYqeBVxo2EqChp5vKYZYEcG8h9XBj4u4NIwg1Mty2JqX30uBjoHvF5w/pMs8lG uvj6JR9I 19wtCuccbAJl 4cUq03UQoIDmwejea oC8A8WJr3vVpODDWrvAsjllGPBECgYEAyQRa6edYO6bsSvgbM13qXW9OQTn9YmgzfN24Ux1D66TQU6sBSLdfSHshDhTCi Ax 698aJNRWujAakA2DDgspSx98aRnHbF zvY7i7iWGesN6uN0zL 6/MK5uWoieGZRjgk230fLk00l4/FK1mJIp0apr0Lis9xmDjP5AaUPTUUCgYEAwXuhTHZWPT6v8YwOksjbuK UDkIIvyMux53kb73vrkgMboS4DB1zMLNyG 9EghS414CFROUwGl4ZUKboH1Jo5G34y8VgDuHjirTqL2H6 zNpML iMrWCXjpFKkxwPbeQnEAZ 5Rud4d PTyXAt71blZHE9tZ4KHy8cU1iKc9APcCgYAIqKZd4vg7AZK2G//X85iv06aUSrIudfyZyVcyRVVyphPPNtOEVVnGXn9rAtvqeIrOo52BR68 cj4vlXp hkDuEH QVBuY/NdQhOzFtPrKPQTJdGjIlQ2x65Vidj7r3sRukNkLPyV2v D885zcpTkp83JFuWTYiIrg275DIuAI3QKBgAglM0IrzS g3vlVQxvM1ussgRgkkYeybHq82 wUW 3DXLqeXb0s1DedplUkuoabZriz0Wh4GZFSmtA5ZpZC uV697lkYsndmp2xRhaekllW7bu pY5q88URwO2p8CO5AZ6CWFWuBwSDML5VOapGRqDRgwaD oGpb7fb7IgHOls7AoGBAJnL6Q8t35uYJ8J8hY7wso88IE04z6VaT8WganxcndesWER9eFQDHDDy//ZYeyt6M41uIY CL Vkm9Kwl/bHLJKdnOE1a9NdE6mtfah0Bk2u/YOuzyu5mmcgZiX X/OZuEbGmmbZOR1FCuIyrNYfwYohhcZP7/r0Ia/1GpkHc3Bi-----END RSA PRIVATE KEY-----","bundle_method":"ubiquitous"}`, string(b)) + } + + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "7e7b8deba8538af625850b7b2530034c", + "hosts": [ + "example.com" + ], + "issuer": "GlobalSign", + "signature": "SHA256WithRSA", + "status": "active", + "bundle_method": "ubiquitous", + "zone_id": "023e105f4ecef8ad9ca31a8372d0c353", + "uploaded_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "expires_on": "2016-01-01T05:20:00Z", + "priority": 1 + } + }`) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/custom_certificates", handler) + + hosts := make([]string, 1, 4) + hosts[0] = "example.com" + uploadedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + expiresOn, _ := time.Parse(time.RFC3339, "2016-01-01T05:20:00Z") + want := ZoneCustomSSL{ + ID: "7e7b8deba8538af625850b7b2530034c", + Hosts: hosts, + Issuer: "GlobalSign", + Signature: "SHA256WithRSA", + Status: "active", + BundleMethod: "ubiquitous", + ZoneID: "023e105f4ecef8ad9ca31a8372d0c353", + UploadedOn: uploadedOn, + ModifiedOn: modifiedOn, + ExpiresOn: expiresOn, + Priority: 1, + } + + actual, err := client.CreateSSL("023e105f4ecef8ad9ca31a8372d0c353", ZoneCustomSSLOptions{ + Certificate: "-----BEGIN CERTIFICATE----- MIIDtTCCAp2gAwIBAgIJAM15n7fdxhRtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX aWRnaXRzIFB0eSBMdGQwHhcNMTQwMzExMTkyMTU5WhcNMTQwNDEwMTkyMTU5WjBF MQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB CgKCAQEAvq3sKsHpeduJHimOK+fvQdKsI8z8A05MZyyLp2/R/GE8FjNv+hkVY1WQ LIyTNNQH7CJecE1nbTfo8Y56S7x/rhxC6/DJ8MIulapFPnorq46KU6yRxiM0MQ3N nTJHlHA2ozZta6YBBfVfhHWl1F0IfNbXCLKvGwWWMbCx43OfW6KTkbRnE6gFWKuO fSO5h2u5TaWVuSIzBvYs7Vza6m+gtYAvKAJV2nSZ+eSEFPDo29corOy8+huEOUL8 5FAw4BFPsr1TlrlGPFitduQUHGrSL7skk1ESGza0to3bOtrodKei2s9bk5MXm7lZ qI+WZJX4Zu9+mzZhc9pCVi8r/qlXuQIDAQABo4GnMIGkMB0GA1UdDgQWBBRvavf+ sWM4IwKiH9X9w1vl6nUVRDB1BgNVHSMEbjBsgBRvavf+sWM4IwKiH9X9w1vl6nUV RKFJpEcwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAM15n7fdxhRtMAwGA1UdEwQF MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBABY2ZzBaW0dMsAAT7tPJzrVWVzQx6KU4 UEBLudIlWPlkAwTnINCWR/8eNjCCmGA4heUdHmazdpPa8RzwOmc0NT1NQqzSyktt vTqb4iHD7+8f9MqJ9/FssCfTtqr/Qst/hGH4Wmdf1EJ/6FqYAAb5iRlPgshFZxU8 uXtA8hWn6fK6eISD9HBdcAFToUvKNZ1BIDPvh9f95Ine8ar6yGd56TUNrHR8eHBs ESxz5ddVR/oWRysNJ+aGAyYqHS8S/ttmC7r4XCAHqXptkHPCGRqkAhsterYhd4I8 /cBzejUobNCjjHFbtkAL/SjxZOLW+pNkZwfeYdM8iPkD54Uua1v2tdw= -----END CERTIFICATE-----", + PrivateKey: "-----BEGIN RSA PRIVATE KEY-----MIIEowIBAAKCAQEAl 1cSc0vfcJLI4ZdWjiZZqy86Eof4czCwilyjXdvHqbdgDjz9H6K/0FX78EzVdfyExESptPCDl5YYjvcZyAWlgNfYEpFpGeoh/pTFW3hlyKImh4EgBXbDrR251J Ew2Nf56X3duibI6X20gKZA6cvdmWeKh MOOXuh1bSPU3dkb4YOF/fng5iGrx0q3txdMQXTPMZ1uXHFcBH7idgViYesXUBhdll3GP1N Y8laq0yrqh 8HMsZK m27MebqonbNmjOqE218lVEvjCdRO6xvNXrO6vNJBoGn2eGwZ8BVd0mTA3Tj43/2cmxQFY9FLq56cCXqYI1fbRRib ZLrjSNkwIDAQABAoIBABfAjjsjjxc0NxcYvKOMUb9Rpj8Sx6U/o/tDC5u XmsGX37aaJmC5yw9BQiAxgvXtQryEl5uoNoqOdsxzKV6yM0vPcwKEJVBd4G6yx6AjVJZnc2qf72erR7BbA2CQh scMDRBKE041HhgTBRNP6roim0SOgYP5JZIrGAQXNIkyE0fZc5gZNUt388ne/mjWM6Xi08BDGurLC68nsdt7Nd UYqeBVxo2EqChp5vKYZYEcG8h9XBj4u4NIwg1Mty2JqX30uBjoHvF5w/pMs8lG uvj6JR9I 19wtCuccbAJl 4cUq03UQoIDmwejea oC8A8WJr3vVpODDWrvAsjllGPBECgYEAyQRa6edYO6bsSvgbM13qXW9OQTn9YmgzfN24Ux1D66TQU6sBSLdfSHshDhTCi Ax 698aJNRWujAakA2DDgspSx98aRnHbF zvY7i7iWGesN6uN0zL 6/MK5uWoieGZRjgk230fLk00l4/FK1mJIp0apr0Lis9xmDjP5AaUPTUUCgYEAwXuhTHZWPT6v8YwOksjbuK UDkIIvyMux53kb73vrkgMboS4DB1zMLNyG 9EghS414CFROUwGl4ZUKboH1Jo5G34y8VgDuHjirTqL2H6 zNpML iMrWCXjpFKkxwPbeQnEAZ 5Rud4d PTyXAt71blZHE9tZ4KHy8cU1iKc9APcCgYAIqKZd4vg7AZK2G//X85iv06aUSrIudfyZyVcyRVVyphPPNtOEVVnGXn9rAtvqeIrOo52BR68 cj4vlXp hkDuEH QVBuY/NdQhOzFtPrKPQTJdGjIlQ2x65Vidj7r3sRukNkLPyV2v D885zcpTkp83JFuWTYiIrg275DIuAI3QKBgAglM0IrzS g3vlVQxvM1ussgRgkkYeybHq82 wUW 3DXLqeXb0s1DedplUkuoabZriz0Wh4GZFSmtA5ZpZC uV697lkYsndmp2xRhaekllW7bu pY5q88URwO2p8CO5AZ6CWFWuBwSDML5VOapGRqDRgwaD oGpb7fb7IgHOls7AoGBAJnL6Q8t35uYJ8J8hY7wso88IE04z6VaT8WganxcndesWER9eFQDHDDy//ZYeyt6M41uIY CL Vkm9Kwl/bHLJKdnOE1a9NdE6mtfah0Bk2u/YOuzyu5mmcgZiX X/OZuEbGmmbZOR1FCuIyrNYfwYohhcZP7/r0Ia/1GpkHc3Bi-----END RSA PRIVATE KEY-----", + BundleMethod: "ubiquitous", + }) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.CreateSSL("bar", ZoneCustomSSLOptions{}) + assert.Error(t, err) +} + +func TestListSSL(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method, "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "id": "7e7b8deba8538af625850b7b2530034c", + "hosts": [ + "example.com" + ], + "issuer": "GlobalSign", + "signature": "SHA256WithRSA", + "status": "active", + "bundle_method": "ubiquitous", + "zone_id": "023e105f4ecef8ad9ca31a8372d0c353", + "uploaded_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "expires_on": "2016-01-01T05:20:00Z", + "priority": 1 + } + ], + "result_info": { + "page": 1, + "per_page": 20, + "count": 1, + "total_count": 2000 + } + }`) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/custom_certificates", handler) + + hosts := make([]string, 1, 4) + hosts[0] = "example.com" + uploadedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + expiresOn, _ := time.Parse(time.RFC3339, "2016-01-01T05:20:00Z") + + want := make([]ZoneCustomSSL, 1, 4) + want[0] = ZoneCustomSSL{ + ID: "7e7b8deba8538af625850b7b2530034c", + Hosts: hosts, + Issuer: "GlobalSign", + Signature: "SHA256WithRSA", + Status: "active", + BundleMethod: "ubiquitous", + ZoneID: "023e105f4ecef8ad9ca31a8372d0c353", + UploadedOn: uploadedOn, + ModifiedOn: modifiedOn, + ExpiresOn: expiresOn, + Priority: 1, + } + + actual, err := client.ListSSL("023e105f4ecef8ad9ca31a8372d0c353") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.ListSSL("bar") + assert.Error(t, err) +} + +func TestSSLDetails(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "7e7b8deba8538af625850b7b2530034c", + "hosts": [ + "example.com" + ], + "issuer": "GlobalSign", + "signature": "SHA256WithRSA", + "status": "active", + "bundle_method": "ubiquitous", + "zone_id": "023e105f4ecef8ad9ca31a8372d0c353", + "uploaded_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "expires_on": "2016-01-01T05:20:00Z", + "priority": 1 + } + }`) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/custom_certificates/7e7b8deba8538af625850b7b2530034c", handler) + + hosts := make([]string, 1, 4) + hosts[0] = "example.com" + uploadedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + expiresOn, _ := time.Parse(time.RFC3339, "2016-01-01T05:20:00Z") + want := ZoneCustomSSL{ + ID: "7e7b8deba8538af625850b7b2530034c", + Hosts: hosts, + Issuer: "GlobalSign", + Signature: "SHA256WithRSA", + Status: "active", + BundleMethod: "ubiquitous", + ZoneID: "023e105f4ecef8ad9ca31a8372d0c353", + UploadedOn: uploadedOn, + ModifiedOn: modifiedOn, + ExpiresOn: expiresOn, + Priority: 1, + } + + actual, err := client.SSLDetails("023e105f4ecef8ad9ca31a8372d0c353", "7e7b8deba8538af625850b7b2530034c") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.SSLDetails("023e105f4ecef8ad9ca31a8372d0c353", "bar") + assert.Error(t, err) +} + +func TestUpdateSSL(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PATCH", r.Method, "Expected method 'PATCH', got %s", r.Method) + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if assert.NoError(t, err) { + assert.JSONEq(t, `{"certificate":"-----BEGIN CERTIFICATE----- MIIDtTCCAp2gAwIBAgIJAM15n7fdxhRtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX aWRnaXRzIFB0eSBMdGQwHhcNMTQwMzExMTkyMTU5WhcNMTQwNDEwMTkyMTU5WjBF MQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB CgKCAQEAvq3sKsHpeduJHimOK+fvQdKsI8z8A05MZyyLp2/R/GE8FjNv+hkVY1WQ LIyTNNQH7CJecE1nbTfo8Y56S7x/rhxC6/DJ8MIulapFPnorq46KU6yRxiM0MQ3N nTJHlHA2ozZta6YBBfVfhHWl1F0IfNbXCLKvGwWWMbCx43OfW6KTkbRnE6gFWKuO fSO5h2u5TaWVuSIzBvYs7Vza6m+gtYAvKAJV2nSZ+eSEFPDo29corOy8+huEOUL8 5FAw4BFPsr1TlrlGPFitduQUHGrSL7skk1ESGza0to3bOtrodKei2s9bk5MXm7lZ qI+WZJX4Zu9+mzZhc9pCVi8r/qlXuQIDAQABo4GnMIGkMB0GA1UdDgQWBBRvavf+ sWM4IwKiH9X9w1vl6nUVRDB1BgNVHSMEbjBsgBRvavf+sWM4IwKiH9X9w1vl6nUV RKFJpEcwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAM15n7fdxhRtMAwGA1UdEwQF MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBABY2ZzBaW0dMsAAT7tPJzrVWVzQx6KU4 UEBLudIlWPlkAwTnINCWR/8eNjCCmGA4heUdHmazdpPa8RzwOmc0NT1NQqzSyktt vTqb4iHD7+8f9MqJ9/FssCfTtqr/Qst/hGH4Wmdf1EJ/6FqYAAb5iRlPgshFZxU8 uXtA8hWn6fK6eISD9HBdcAFToUvKNZ1BIDPvh9f95Ine8ar6yGd56TUNrHR8eHBs ESxz5ddVR/oWRysNJ+aGAyYqHS8S/ttmC7r4XCAHqXptkHPCGRqkAhsterYhd4I8 /cBzejUobNCjjHFbtkAL/SjxZOLW+pNkZwfeYdM8iPkD54Uua1v2tdw= -----END CERTIFICATE-----","private_key":"-----BEGIN RSA PRIVATE KEY-----MIIEowIBAAKCAQEAl 1cSc0vfcJLI4ZdWjiZZqy86Eof4czCwilyjXdvHqbdgDjz9H6K/0FX78EzVdfyExESptPCDl5YYjvcZyAWlgNfYEpFpGeoh/pTFW3hlyKImh4EgBXbDrR251J Ew2Nf56X3duibI6X20gKZA6cvdmWeKh MOOXuh1bSPU3dkb4YOF/fng5iGrx0q3txdMQXTPMZ1uXHFcBH7idgViYesXUBhdll3GP1N Y8laq0yrqh 8HMsZK m27MebqonbNmjOqE218lVEvjCdRO6xvNXrO6vNJBoGn2eGwZ8BVd0mTA3Tj43/2cmxQFY9FLq56cCXqYI1fbRRib ZLrjSNkwIDAQABAoIBABfAjjsjjxc0NxcYvKOMUb9Rpj8Sx6U/o/tDC5u XmsGX37aaJmC5yw9BQiAxgvXtQryEl5uoNoqOdsxzKV6yM0vPcwKEJVBd4G6yx6AjVJZnc2qf72erR7BbA2CQh scMDRBKE041HhgTBRNP6roim0SOgYP5JZIrGAQXNIkyE0fZc5gZNUt388ne/mjWM6Xi08BDGurLC68nsdt7Nd UYqeBVxo2EqChp5vKYZYEcG8h9XBj4u4NIwg1Mty2JqX30uBjoHvF5w/pMs8lG uvj6JR9I 19wtCuccbAJl 4cUq03UQoIDmwejea oC8A8WJr3vVpODDWrvAsjllGPBECgYEAyQRa6edYO6bsSvgbM13qXW9OQTn9YmgzfN24Ux1D66TQU6sBSLdfSHshDhTCi Ax 698aJNRWujAakA2DDgspSx98aRnHbF zvY7i7iWGesN6uN0zL 6/MK5uWoieGZRjgk230fLk00l4/FK1mJIp0apr0Lis9xmDjP5AaUPTUUCgYEAwXuhTHZWPT6v8YwOksjbuK UDkIIvyMux53kb73vrkgMboS4DB1zMLNyG 9EghS414CFROUwGl4ZUKboH1Jo5G34y8VgDuHjirTqL2H6 zNpML iMrWCXjpFKkxwPbeQnEAZ 5Rud4d PTyXAt71blZHE9tZ4KHy8cU1iKc9APcCgYAIqKZd4vg7AZK2G//X85iv06aUSrIudfyZyVcyRVVyphPPNtOEVVnGXn9rAtvqeIrOo52BR68 cj4vlXp hkDuEH QVBuY/NdQhOzFtPrKPQTJdGjIlQ2x65Vidj7r3sRukNkLPyV2v D885zcpTkp83JFuWTYiIrg275DIuAI3QKBgAglM0IrzS g3vlVQxvM1ussgRgkkYeybHq82 wUW 3DXLqeXb0s1DedplUkuoabZriz0Wh4GZFSmtA5ZpZC uV697lkYsndmp2xRhaekllW7bu pY5q88URwO2p8CO5AZ6CWFWuBwSDML5VOapGRqDRgwaD oGpb7fb7IgHOls7AoGBAJnL6Q8t35uYJ8J8hY7wso88IE04z6VaT8WganxcndesWER9eFQDHDDy//ZYeyt6M41uIY CL Vkm9Kwl/bHLJKdnOE1a9NdE6mtfah0Bk2u/YOuzyu5mmcgZiX X/OZuEbGmmbZOR1FCuIyrNYfwYohhcZP7/r0Ia/1GpkHc3Bi-----END RSA PRIVATE KEY-----","bundle_method":"ubiquitous"}`, string(b)) + } + + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "7e7b8deba8538af625850b7b2530034c", + "hosts": [ + "example.com" + ], + "issuer": "GlobalSign", + "signature": "SHA256WithRSA", + "status": "active", + "bundle_method": "ubiquitous", + "zone_id": "023e105f4ecef8ad9ca31a8372d0c353", + "uploaded_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "expires_on": "2016-01-01T05:20:00Z", + "priority": 1 + } + }`) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/custom_certificates/7e7b8deba8538af625850b7b2530034c", handler) + + hosts := make([]string, 1, 4) + hosts[0] = "example.com" + uploadedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + expiresOn, _ := time.Parse(time.RFC3339, "2016-01-01T05:20:00Z") + want := ZoneCustomSSL{ + ID: "7e7b8deba8538af625850b7b2530034c", + Hosts: hosts, + Issuer: "GlobalSign", + Signature: "SHA256WithRSA", + Status: "active", + BundleMethod: "ubiquitous", + ZoneID: "023e105f4ecef8ad9ca31a8372d0c353", + UploadedOn: uploadedOn, + ModifiedOn: modifiedOn, + ExpiresOn: expiresOn, + Priority: 1, + } + + actual, err := client.UpdateSSL("023e105f4ecef8ad9ca31a8372d0c353", "7e7b8deba8538af625850b7b2530034c", ZoneCustomSSLOptions{ + Certificate: "-----BEGIN CERTIFICATE----- MIIDtTCCAp2gAwIBAgIJAM15n7fdxhRtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX aWRnaXRzIFB0eSBMdGQwHhcNMTQwMzExMTkyMTU5WhcNMTQwNDEwMTkyMTU5WjBF MQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB CgKCAQEAvq3sKsHpeduJHimOK+fvQdKsI8z8A05MZyyLp2/R/GE8FjNv+hkVY1WQ LIyTNNQH7CJecE1nbTfo8Y56S7x/rhxC6/DJ8MIulapFPnorq46KU6yRxiM0MQ3N nTJHlHA2ozZta6YBBfVfhHWl1F0IfNbXCLKvGwWWMbCx43OfW6KTkbRnE6gFWKuO fSO5h2u5TaWVuSIzBvYs7Vza6m+gtYAvKAJV2nSZ+eSEFPDo29corOy8+huEOUL8 5FAw4BFPsr1TlrlGPFitduQUHGrSL7skk1ESGza0to3bOtrodKei2s9bk5MXm7lZ qI+WZJX4Zu9+mzZhc9pCVi8r/qlXuQIDAQABo4GnMIGkMB0GA1UdDgQWBBRvavf+ sWM4IwKiH9X9w1vl6nUVRDB1BgNVHSMEbjBsgBRvavf+sWM4IwKiH9X9w1vl6nUV RKFJpEcwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAM15n7fdxhRtMAwGA1UdEwQF MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBABY2ZzBaW0dMsAAT7tPJzrVWVzQx6KU4 UEBLudIlWPlkAwTnINCWR/8eNjCCmGA4heUdHmazdpPa8RzwOmc0NT1NQqzSyktt vTqb4iHD7+8f9MqJ9/FssCfTtqr/Qst/hGH4Wmdf1EJ/6FqYAAb5iRlPgshFZxU8 uXtA8hWn6fK6eISD9HBdcAFToUvKNZ1BIDPvh9f95Ine8ar6yGd56TUNrHR8eHBs ESxz5ddVR/oWRysNJ+aGAyYqHS8S/ttmC7r4XCAHqXptkHPCGRqkAhsterYhd4I8 /cBzejUobNCjjHFbtkAL/SjxZOLW+pNkZwfeYdM8iPkD54Uua1v2tdw= -----END CERTIFICATE-----", + PrivateKey: "-----BEGIN RSA PRIVATE KEY-----MIIEowIBAAKCAQEAl 1cSc0vfcJLI4ZdWjiZZqy86Eof4czCwilyjXdvHqbdgDjz9H6K/0FX78EzVdfyExESptPCDl5YYjvcZyAWlgNfYEpFpGeoh/pTFW3hlyKImh4EgBXbDrR251J Ew2Nf56X3duibI6X20gKZA6cvdmWeKh MOOXuh1bSPU3dkb4YOF/fng5iGrx0q3txdMQXTPMZ1uXHFcBH7idgViYesXUBhdll3GP1N Y8laq0yrqh 8HMsZK m27MebqonbNmjOqE218lVEvjCdRO6xvNXrO6vNJBoGn2eGwZ8BVd0mTA3Tj43/2cmxQFY9FLq56cCXqYI1fbRRib ZLrjSNkwIDAQABAoIBABfAjjsjjxc0NxcYvKOMUb9Rpj8Sx6U/o/tDC5u XmsGX37aaJmC5yw9BQiAxgvXtQryEl5uoNoqOdsxzKV6yM0vPcwKEJVBd4G6yx6AjVJZnc2qf72erR7BbA2CQh scMDRBKE041HhgTBRNP6roim0SOgYP5JZIrGAQXNIkyE0fZc5gZNUt388ne/mjWM6Xi08BDGurLC68nsdt7Nd UYqeBVxo2EqChp5vKYZYEcG8h9XBj4u4NIwg1Mty2JqX30uBjoHvF5w/pMs8lG uvj6JR9I 19wtCuccbAJl 4cUq03UQoIDmwejea oC8A8WJr3vVpODDWrvAsjllGPBECgYEAyQRa6edYO6bsSvgbM13qXW9OQTn9YmgzfN24Ux1D66TQU6sBSLdfSHshDhTCi Ax 698aJNRWujAakA2DDgspSx98aRnHbF zvY7i7iWGesN6uN0zL 6/MK5uWoieGZRjgk230fLk00l4/FK1mJIp0apr0Lis9xmDjP5AaUPTUUCgYEAwXuhTHZWPT6v8YwOksjbuK UDkIIvyMux53kb73vrkgMboS4DB1zMLNyG 9EghS414CFROUwGl4ZUKboH1Jo5G34y8VgDuHjirTqL2H6 zNpML iMrWCXjpFKkxwPbeQnEAZ 5Rud4d PTyXAt71blZHE9tZ4KHy8cU1iKc9APcCgYAIqKZd4vg7AZK2G//X85iv06aUSrIudfyZyVcyRVVyphPPNtOEVVnGXn9rAtvqeIrOo52BR68 cj4vlXp hkDuEH QVBuY/NdQhOzFtPrKPQTJdGjIlQ2x65Vidj7r3sRukNkLPyV2v D885zcpTkp83JFuWTYiIrg275DIuAI3QKBgAglM0IrzS g3vlVQxvM1ussgRgkkYeybHq82 wUW 3DXLqeXb0s1DedplUkuoabZriz0Wh4GZFSmtA5ZpZC uV697lkYsndmp2xRhaekllW7bu pY5q88URwO2p8CO5AZ6CWFWuBwSDML5VOapGRqDRgwaD oGpb7fb7IgHOls7AoGBAJnL6Q8t35uYJ8J8hY7wso88IE04z6VaT8WganxcndesWER9eFQDHDDy//ZYeyt6M41uIY CL Vkm9Kwl/bHLJKdnOE1a9NdE6mtfah0Bk2u/YOuzyu5mmcgZiX X/OZuEbGmmbZOR1FCuIyrNYfwYohhcZP7/r0Ia/1GpkHc3Bi-----END RSA PRIVATE KEY-----", + BundleMethod: "ubiquitous", + }) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } + + _, err = client.UpdateSSL("023e105f4ecef8ad9ca31a8372d0c353", "bar", ZoneCustomSSLOptions{}) + assert.Error(t, err) +} + +func TestReprioritizeSSL(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method, "Expected method 'PUT', got %s", r.Method) + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if assert.NoError(t, err) { + assert.JSONEq(t, `{"certificates":[{"ID":"5a7805061c76ada191ed06f989cc3dac","priority":2},{"ID":"9a7806061c88ada191ed06f989cc3dac","priority":1}]}`, string(b)) + } + + w.Header().Set("content-type", "application/json") + // XXX: Test response flow properly. + // Current response assertion uses generic example from the documentation, + // rather than responding to the actual PUT request. + // https://api.cloudflare.com/#custom-ssl-for-a-zone-re-prioritize-ssl-certificates + fmt.Fprint(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "id": "7e7b8deba8538af625850b7b2530034c", + "hosts": [ + "example.com" + ], + "issuer": "GlobalSign", + "signature": "SHA256WithRSA", + "status": "active", + "bundle_method": "ubiquitous", + "zone_id": "023e105f4ecef8ad9ca31a8372d0c353", + "uploaded_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "expires_on": "2016-01-01T05:20:00Z", + "priority": 1 + } + ], + "result_info": { + "page": 1, + "per_page": 20, + "count": 1, + "total_count": 2000 + } + }`) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/custom_certificates/prioritize", handler) + + hosts := make([]string, 1, 4) + hosts[0] = "example.com" + uploadedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + expiresOn, _ := time.Parse(time.RFC3339, "2016-01-01T05:20:00Z") + + want := make([]ZoneCustomSSL, 1, 4) + want[0] = ZoneCustomSSL{ + ID: "7e7b8deba8538af625850b7b2530034c", + Hosts: hosts, + Issuer: "GlobalSign", + Signature: "SHA256WithRSA", + Status: "active", + BundleMethod: "ubiquitous", + ZoneID: "023e105f4ecef8ad9ca31a8372d0c353", + UploadedOn: uploadedOn, + ModifiedOn: modifiedOn, + ExpiresOn: expiresOn, + Priority: 1, + } + + actual, err := client.ReprioritizeSSL("023e105f4ecef8ad9ca31a8372d0c353", []ZoneCustomSSLPriority{ + {ID: "5a7805061c76ada191ed06f989cc3dac", Priority: 2}, + {ID: "9a7806061c88ada191ed06f989cc3dac", Priority: 1}, + }) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestDeleteSSL(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method, "Expected method 'DELETE', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "id": "7e7b8deba8538af625850b7b2530034c" + }`) + } + + mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/custom_certificates/7e7b8deba8538af625850b7b2530034c", handler) + + err := client.DeleteSSL("023e105f4ecef8ad9ca31a8372d0c353", "7e7b8deba8538af625850b7b2530034c") + assert.NoError(t, err, "Expected to successfully delete certificate ID '7e7b8deba8538af625850b7b2530034c', received error instead") + + err = client.DeleteSSL("023e105f4ecef8ad9ca31a8372d0c353", "bar") + assert.Error(t, err, "Expected to error when attempting to delete certificate ID 'bar', did not receive error instead") +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/user.go b/vendor/github.com/cloudflare/cloudflare-go/user.go new file mode 100644 index 000000000..a498beeca --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/user.go @@ -0,0 +1,116 @@ +package cloudflare + +import ( + "encoding/json" + "time" + + "github.com/pkg/errors" +) + +// User describes a user account. +type User struct { + ID string `json:"id,omitempty"` + Email string `json:"email,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Username string `json:"username,omitempty"` + Telephone string `json:"telephone,omitempty"` + Country string `json:"country,omitempty"` + Zipcode string `json:"zipcode,omitempty"` + CreatedOn *time.Time `json:"created_on,omitempty"` + ModifiedOn *time.Time `json:"modified_on,omitempty"` + APIKey string `json:"api_key,omitempty"` + TwoFA bool `json:"two_factor_authentication_enabled,omitempty"` + Betas []string `json:"betas,omitempty"` + Organizations []Organization `json:"organizations,omitempty"` +} + +// UserResponse wraps a response containing User accounts. +type UserResponse struct { + Response + Result User `json:"result"` +} + +// userBillingProfileResponse wraps a response containing Billing Profile information. +type userBillingProfileResponse struct { + Response + Result UserBillingProfile +} + +// UserBillingProfile contains Billing Profile information. +type UserBillingProfile struct { + ID string `json:"id,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Address string `json:"address,omitempty"` + Address2 string `json:"address2,omitempty"` + Company string `json:"company,omitempty"` + City string `json:"city,omitempty"` + State string `json:"state,omitempty"` + ZipCode string `json:"zipcode,omitempty"` + Country string `json:"country,omitempty"` + Telephone string `json:"telephone,omitempty"` + CardNumber string `json:"card_number,omitempty"` + CardExpiryYear int `json:"card_expiry_year,omitempty"` + CardExpiryMonth int `json:"card_expiry_month,omitempty"` + VAT string `json:"vat,omitempty"` + CreatedOn *time.Time `json:"created_on,omitempty"` + EditedOn *time.Time `json:"edited_on,omitempty"` +} + +// UserDetails provides information about the logged-in user. +// API reference: +// https://api.cloudflare.com/#user-user-details +// GET /user +func (api *API) UserDetails() (User, error) { + var r UserResponse + res, err := api.makeRequest("GET", "/user", nil) + if err != nil { + return User{}, errors.Wrap(err, errMakeRequestError) + } + + err = json.Unmarshal(res, &r) + if err != nil { + return User{}, errors.Wrap(err, errUnmarshalError) + } + + return r.Result, nil +} + +// UpdateUser updates the properties of the given user. +// API reference: +// https://api.cloudflare.com/#user-update-user +// PATCH /user +func (api *API) UpdateUser(user *User) (User, error) { + var r UserResponse + res, err := api.makeRequest("PATCH", "/user", user) + if err != nil { + return User{}, errors.Wrap(err, errMakeRequestError) + } + + err = json.Unmarshal(res, &r) + if err != nil { + return User{}, errors.Wrap(err, errUnmarshalError) + } + + return r.Result, nil +} + +// UserBillingProfile returns the billing profile of the user. +// API reference: +// https://api.cloudflare.com/#user-billing-profile +// GET /user/billing/profile +func (api *API) UserBillingProfile() (UserBillingProfile, error) { + var r userBillingProfileResponse + res, err := api.makeRequest("GET", "/user/billing/profile", nil) + if err != nil { + return UserBillingProfile{}, errors.Wrap(err, errMakeRequestError) + } + + err = json.Unmarshal(res, &r) + if err != nil { + return UserBillingProfile{}, errors.Wrap(err, errUnmarshalError) + } + + return r.Result, nil +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/user_test.go b/vendor/github.com/cloudflare/cloudflare-go/user_test.go new file mode 100644 index 000000000..061fe9e2c --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/user_test.go @@ -0,0 +1,196 @@ +package cloudflare + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "io/ioutil" +) + +func TestUser_UserDetails(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method, "Expected method 'GET', got %s", r.Method) + + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ +"success": true, +"errors": [], +"messages": [], +"result": { + "id": "1", + "email": "cloudflare@example.com", + "first_name": "Jane", + "last_name": "Smith", + "username": "cloudflare12345", + "telephone": "+1 (650) 319 8930", + "country": "US", + "zipcode": "94107", + "created_on": "2009-07-01T00:00:00Z", + "modified_on": "2016-05-06T20:32:00Z", + "two_factor_authentication_enabled": true, + "betas": ["mirage_forever"] + } +}`) + }) + + user, err := client.UserDetails() + + createdOn, _ := time.Parse(time.RFC3339, "2009-07-01T00:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2016-05-06T20:32:00Z") + + want := User{ + ID: "1", + Email: "cloudflare@example.com", + FirstName: "Jane", + LastName: "Smith", + Username: "cloudflare12345", + Telephone: "+1 (650) 319 8930", + Country: "US", + Zipcode: "94107", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + TwoFA: true, + Betas: []string{"mirage_forever"}, + } + + if assert.NoError(t, err) { + assert.Equal(t, user, want) + } +} + +func TestUser_UpdateUser(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PATCH", r.Method, "Expected method 'PATCH', got %s", r.Method) + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if assert.NoError(t, err) { + assert.JSONEq(t, `{"country":"US","first_name":"John","username":"cfuser12345","email":"user@example.com", + "last_name": "Appleseed","telephone": "+1 123-123-1234","zipcode": "12345"}`, string(b), "JSON not equal") + } + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "7c5dae5552338874e5053f2534d2767a", + "email": "user@example.com", + "first_name": "John", + "last_name": "Appleseed", + "username": "cfuser12345", + "telephone": "+1 123-123-1234", + "country": "US", + "zipcode": "12345", + "created_on": "2014-01-01T05:20:00Z", + "modified_on": "2014-01-01T05:20:00Z", + "two_factor_authentication_enabled": false + } +}`) + }) + + createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z") + + userIn := User{ + Email: "user@example.com", + FirstName: "John", + LastName: "Appleseed", + Username: "cfuser12345", + Telephone: "+1 123-123-1234", + Country: "US", + Zipcode: "12345", + TwoFA: false, + } + + userOut, err := client.UpdateUser(&userIn) + + want := User{ + ID: "7c5dae5552338874e5053f2534d2767a", + Email: "user@example.com", + FirstName: "John", + LastName: "Appleseed", + Username: "cfuser12345", + Telephone: "+1 123-123-1234", + Country: "US", + Zipcode: "12345", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + TwoFA: false, + } + + if assert.NoError(t, err) { + assert.Equal(t, userOut, want, "structs not equal") + } +} + +func TestUser_UserBillingProfile(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/user/billing/profile", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method, "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "0020c268dbf54e975e7fe8563df49d52", + "first_name": "Bob", + "last_name": "Smith", + "address": "123 3rd St.", + "address2": "Apt 123", + "company": "Cloudflare", + "city": "San Francisco", + "state": "CA", + "zipcode": "12345", + "country": "US", + "telephone": "+1 111-867-5309", + "card_number": "xxxx-xxxx-xxxx-1234", + "card_expiry_year": 2015, + "card_expiry_month": 4, + "vat": "aaa-123-987", + "edited_on": "2014-04-01T12:21:02.0000Z", + "created_on": "2014-03-01T12:21:02.0000Z" + } +}`) + }) + + createdOn, _ := time.Parse(time.RFC3339, "2014-03-01T12:21:02.0000Z") + editedOn, _ := time.Parse(time.RFC3339, "2014-04-01T12:21:02.0000Z") + + userBillingProfile, err := client.UserBillingProfile() + + want := UserBillingProfile{ + ID: "0020c268dbf54e975e7fe8563df49d52", + FirstName: "Bob", + LastName: "Smith", + Address: "123 3rd St.", + Address2: "Apt 123", + Company: "Cloudflare", + City: "San Francisco", + State: "CA", + ZipCode: "12345", + Country: "US", + Telephone: "+1 111-867-5309", + CardNumber: "xxxx-xxxx-xxxx-1234", + CardExpiryYear: 2015, + CardExpiryMonth: 4, + VAT: "aaa-123-987", + CreatedOn: &createdOn, + EditedOn: &editedOn, + } + + if assert.NoError(t, err) { + assert.Equal(t, userBillingProfile, want, "structs not equal") + } +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/virtualdns.go b/vendor/github.com/cloudflare/cloudflare-go/virtualdns.go new file mode 100644 index 000000000..a0db270b6 --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/virtualdns.go @@ -0,0 +1,130 @@ +package cloudflare + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +// VirtualDNS represents a Virtual DNS configuration. +type VirtualDNS struct { + ID string `json:"id"` + Name string `json:"name"` + OriginIPs []string `json:"origin_ips"` + VirtualDNSIPs []string `json:"virtual_dns_ips"` + MinimumCacheTTL uint `json:"minimum_cache_ttl"` + MaximumCacheTTL uint `json:"maximum_cache_ttl"` + DeprecateAnyRequests bool `json:"deprecate_any_requests"` + ModifiedOn string `json:"modified_on"` +} + +// VirtualDNSResponse represents a Virtual DNS response. +type VirtualDNSResponse struct { + Response + Result *VirtualDNS `json:"result"` +} + +// VirtualDNSListResponse represents an array of Virtual DNS responses. +type VirtualDNSListResponse struct { + Response + Result []*VirtualDNS `json:"result"` +} + +// CreateVirtualDNS creates a new Virtual DNS cluster. +// API reference: +// https://api.cloudflare.com/#virtual-dns-users--create-a-virtual-dns-cluster +// POST /user/virtual_dns +func (api *API) CreateVirtualDNS(v *VirtualDNS) (*VirtualDNS, error) { + res, err := api.makeRequest("POST", "/user/virtual_dns", v) + if err != nil { + return nil, errors.Wrap(err, errMakeRequestError) + } + + response := &VirtualDNSResponse{} + err = json.Unmarshal(res, &response) + if err != nil { + return nil, errors.Wrap(err, errUnmarshalError) + } + + return response.Result, nil +} + +// VirtualDNS fetches a single virtual DNS cluster. +// API reference: +// https://api.cloudflare.com/#virtual-dns-users--get-a-virtual-dns-cluster +// GET /user/virtual_dns/:identifier +func (api *API) VirtualDNS(virtualDNSID string) (*VirtualDNS, error) { + uri := "/user/virtual_dns/" + virtualDNSID + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return nil, errors.Wrap(err, errMakeRequestError) + } + + response := &VirtualDNSResponse{} + err = json.Unmarshal(res, &response) + if err != nil { + return nil, errors.Wrap(err, errUnmarshalError) + } + + return response.Result, nil +} + +// ListVirtualDNS lists the virtual DNS clusters associated with an account. +// API reference: +// https://api.cloudflare.com/#virtual-dns-users--get-virtual-dns-clusters +// GET /user/virtual_dns +func (api *API) ListVirtualDNS() ([]*VirtualDNS, error) { + res, err := api.makeRequest("GET", "/user/virtual_dns", nil) + if err != nil { + return nil, errors.Wrap(err, errMakeRequestError) + } + + response := &VirtualDNSListResponse{} + err = json.Unmarshal(res, &response) + if err != nil { + return nil, errors.Wrap(err, errUnmarshalError) + } + + return response.Result, nil +} + +// UpdateVirtualDNS updates a Virtual DNS cluster. +// API reference: +// https://api.cloudflare.com/#virtual-dns-users--modify-a-virtual-dns-cluster +// PATCH /user/virtual_dns/:identifier +func (api *API) UpdateVirtualDNS(virtualDNSID string, vv VirtualDNS) error { + uri := "/user/virtual_dns/" + virtualDNSID + res, err := api.makeRequest("PUT", uri, vv) + if err != nil { + return errors.Wrap(err, errMakeRequestError) + } + + response := &VirtualDNSResponse{} + err = json.Unmarshal(res, &response) + if err != nil { + return errors.Wrap(err, errUnmarshalError) + } + + return nil +} + +// DeleteVirtualDNS deletes a Virtual DNS cluster. Note that this cannot be +// undone, and will stop all traffic to that cluster. +// API reference: +// https://api.cloudflare.com/#virtual-dns-users--delete-a-virtual-dns-cluster +// DELETE /user/virtual_dns/:identifier +func (api *API) DeleteVirtualDNS(virtualDNSID string) error { + uri := "/user/virtual_dns/" + virtualDNSID + res, err := api.makeRequest("DELETE", uri, nil) + if err != nil { + return errors.Wrap(err, errMakeRequestError) + } + + response := &VirtualDNSResponse{} + err = json.Unmarshal(res, &response) + if err != nil { + return errors.Wrap(err, errUnmarshalError) + } + + return nil +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/waf.go b/vendor/github.com/cloudflare/cloudflare-go/waf.go new file mode 100644 index 000000000..4ed156236 --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/waf.go @@ -0,0 +1,97 @@ +package cloudflare + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +// WAFPackage represents a WAF package configuration. +type WAFPackage struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ZoneID string `json:"zone_id"` + DetectionMode string `json:"detection_mode"` + Sensitivity string `json:"sensitivity"` + ActionMode string `json:"action_mode"` +} + +// WAFPackagesResponse represents the response from the WAF packages endpoint. +type WAFPackagesResponse struct { + Response + Result []WAFPackage `json:"result"` + ResultInfo ResultInfo `json:"result_info"` +} + +// WAFRule represents a WAF rule. +type WAFRule struct { + ID string `json:"id"` + Description string `json:"description"` + Priority string `json:"priority"` + PackageID string `json:"package_id"` + Group struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"group"` + Mode string `json:"mode"` + DefaultMode string `json:"default_mode"` + AllowedModes []string `json:"allowed_modes"` +} + +// WAFRulesResponse represents the response from the WAF rule endpoint. +type WAFRulesResponse struct { + Response + Result []WAFRule `json:"result"` + ResultInfo ResultInfo `json:"result_info"` +} + +// ListWAFPackages returns a slice of the WAF packages for the given zone. +func (api *API) ListWAFPackages(zoneID string) ([]WAFPackage, error) { + var p WAFPackagesResponse + var packages []WAFPackage + var res []byte + var err error + uri := "/zones/" + zoneID + "/firewall/waf/packages" + res, err = api.makeRequest("GET", uri, nil) + if err != nil { + return []WAFPackage{}, errors.Wrap(err, errMakeRequestError) + } + err = json.Unmarshal(res, &p) + if err != nil { + return []WAFPackage{}, errors.Wrap(err, errUnmarshalError) + } + if !p.Success { + // TODO: Provide an actual error message instead of always returning nil + return []WAFPackage{}, err + } + for pi := range p.Result { + packages = append(packages, p.Result[pi]) + } + return packages, nil +} + +// ListWAFRules returns a slice of the WAF rules for the given WAF package. +func (api *API) ListWAFRules(zoneID, packageID string) ([]WAFRule, error) { + var r WAFRulesResponse + var rules []WAFRule + var res []byte + var err error + uri := "/zones/" + zoneID + "/firewall/waf/packages/" + packageID + "/rules" + res, err = api.makeRequest("GET", uri, nil) + if err != nil { + return []WAFRule{}, errors.Wrap(err, errMakeRequestError) + } + err = json.Unmarshal(res, &r) + if err != nil { + return []WAFRule{}, errors.Wrap(err, errUnmarshalError) + } + if !r.Success { + // TODO: Provide an actual error message instead of always returning nil + return []WAFRule{}, err + } + for ri := range r.Result { + rules = append(rules, r.Result[ri]) + } + return rules, nil +} diff --git a/vendor/github.com/cloudflare/cloudflare-go/zone.go b/vendor/github.com/cloudflare/cloudflare-go/zone.go new file mode 100644 index 000000000..13ce797cb --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/zone.go @@ -0,0 +1,521 @@ +package cloudflare + +import ( + "encoding/json" + "fmt" + "net/url" + "time" + + "github.com/pkg/errors" +) + +// Owner describes the resource owner. +type Owner struct { + ID string `json:"id"` + Email string `json:"email"` + OwnerType string `json:"owner_type"` +} + +// Zone describes a Cloudflare zone. +type Zone struct { + ID string `json:"id"` + Name string `json:"name"` + DevMode int `json:"development_mode"` + OriginalNS []string `json:"original_name_servers"` + OriginalRegistrar string `json:"original_registrar"` + OriginalDNSHost string `json:"original_dnshost"` + CreatedOn time.Time `json:"created_on"` + ModifiedOn time.Time `json:"modified_on"` + NameServers []string `json:"name_servers"` + Owner Owner `json:"owner"` + Permissions []string `json:"permissions"` + Plan ZonePlan `json:"plan"` + PlanPending ZonePlan `json:"plan_pending,omitempty"` + Status string `json:"status"` + Paused bool `json:"paused"` + Type string `json:"type"` + Host struct { + Name string + Website string + } `json:"host"` + VanityNS []string `json:"vanity_name_servers"` + Betas []string `json:"betas"` + DeactReason string `json:"deactivation_reason"` + Meta ZoneMeta `json:"meta"` +} + +// ZoneMeta metadata about a zone. +type ZoneMeta struct { + // custom_certificate_quota is broken - sometimes it's a string, sometimes a number! + // CustCertQuota int `json:"custom_certificate_quota"` + PageRuleQuota int `json:"page_rule_quota"` + WildcardProxiable bool `json:"wildcard_proxiable"` + PhishingDetected bool `json:"phishing_detected"` +} + +// ZonePlan contains the plan information for a zone. +type ZonePlan struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + Price int `json:"price,omitempty"` + Currency string `json:"currency,omitempty"` + Frequency string `json:"frequency,omitempty"` + LegacyID string `json:"legacy_id,omitempty"` + IsSubscribed bool `json:"is_subscribed,omitempty"` + CanSubscribe bool `json:"can_subscribe,omitempty"` +} + +// ZoneID contains only the zone ID. +type ZoneID struct { + ID string `json:"id"` +} + +// ZoneResponse represents the response from the Zone endpoint containing a single zone. +type ZoneResponse struct { + Response + Result Zone `json:"result"` +} + +// ZonesResponse represents the response from the Zone endpoint containing an array of zones. +type ZonesResponse struct { + Response + Result []Zone `json:"result"` +} + +// ZoneIDResponse represents the response from the Zone endpoint, containing only a zone ID. +type ZoneIDResponse struct { + Response + Result ZoneID `json:"result"` +} + +// AvailableZonePlansResponse represents the response from the Available Plans endpoint. +type AvailableZonePlansResponse struct { + Response + Result []ZonePlan `json:"result"` + ResultInfo +} + +// ZonePlanResponse represents the response from the Plan Details endpoint. +type ZonePlanResponse struct { + Response + Result ZonePlan `json:"result"` +} + +// ZoneSetting contains settings for a zone. +type ZoneSetting struct { + ID string `json:"id"` + Editable bool `json:"editable"` + ModifiedOn string `json:"modified_on"` + Value interface{} `json:"value"` + TimeRemaining int `json:"time_remaining"` +} + +// ZoneSettingResponse represents the response from the Zone Setting endpoint. +type ZoneSettingResponse struct { + Response + Result []ZoneSetting `json:"result"` +} + +// ZoneAnalyticsData contains totals and timeseries analytics data for a zone. +type ZoneAnalyticsData struct { + Totals ZoneAnalytics `json:"totals"` + Timeseries []ZoneAnalytics `json:"timeseries"` +} + +// zoneAnalyticsDataResponse represents the response from the Zone Analytics Dashboard endpoint. +type zoneAnalyticsDataResponse struct { + Response + Result ZoneAnalyticsData `json:"result"` +} + +// ZoneAnalyticsColocation contains analytics data by datacenter. +type ZoneAnalyticsColocation struct { + ColocationID string `json:"colo_id"` + Timeseries []ZoneAnalytics `json:"timeseries"` +} + +// zoneAnalyticsColocationResponse represents the response from the Zone Analytics By Co-location endpoint. +type zoneAnalyticsColocationResponse struct { + Response + Result []ZoneAnalyticsColocation `json:"result"` +} + +// ZoneAnalytics contains analytics data for a zone. +type ZoneAnalytics struct { + Since time.Time `json:"since"` + Until time.Time `json:"until"` + Requests struct { + All int `json:"all"` + Cached int `json:"cached"` + Uncached int `json:"uncached"` + ContentType map[string]int `json:"content_type"` + Country map[string]int `json:"country"` + SSL struct { + Encrypted int `json:"encrypted"` + Unencrypted int `json:"unencrypted"` + } `json:"ssl"` + HTTPStatus map[string]int `json:"http_status"` + } `json:"requests"` + Bandwidth struct { + All int `json:"all"` + Cached int `json:"cached"` + Uncached int `json:"uncached"` + ContentType map[string]int `json:"content_type"` + Country map[string]int `json:"country"` + SSL struct { + Encrypted int `json:"encrypted"` + Unencrypted int `json:"unencrypted"` + } `json:"ssl"` + } `json:"bandwidth"` + Threats struct { + All int `json:"all"` + Country map[string]int `json:"country"` + Type map[string]int `json:"type"` + } `json:"threats"` + Pageviews struct { + All int `json:"all"` + SearchEngines map[string]int `json:"search_engines"` + } `json:"pageviews"` + Uniques struct { + All int `json:"all"` + } +} + +// ZoneAnalyticsOptions represents the optional parameters in Zone Analytics +// endpoint requests. +type ZoneAnalyticsOptions struct { + Since *time.Time + Until *time.Time + Continuous *bool +} + +// PurgeCacheRequest represents the request format made to the purge endpoint. +type PurgeCacheRequest struct { + Everything bool `json:"purge_everything,omitempty"` + Files []string `json:"files,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +// PurgeCacheResponse represents the response from the purge endpoint. +type PurgeCacheResponse struct { + Response +} + +// newZone describes a new zone. +type newZone struct { + Name string `json:"name"` + JumpStart bool `json:"jump_start"` + // We use a pointer to get a nil type when the field is empty. + // This allows us to completely omit this with json.Marshal(). + Organization *Organization `json:"organization,omitempty"` +} + +// CreateZone creates a zone on an account. +// +// API reference: https://api.cloudflare.com/#zone-create-a-zone +func (api *API) CreateZone(name string, jumpstart bool, org Organization) (Zone, error) { + var newzone newZone + newzone.Name = name + newzone.JumpStart = jumpstart + if org.ID != "" { + newzone.Organization = &org + } + + res, err := api.makeRequest("POST", "/zones", newzone) + if err != nil { + return Zone{}, errors.Wrap(err, errMakeRequestError) + } + + var r ZoneResponse + err = json.Unmarshal(res, &r) + if err != nil { + return Zone{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// ZoneActivationCheck initiates another zone activation check for newly-created zones. +// +// API reference: https://api.cloudflare.com/#zone-initiate-another-zone-activation-check +func (api *API) ZoneActivationCheck(zoneID string) (Response, error) { + res, err := api.makeRequest("PUT", "/zones/"+zoneID+"/activation_check", nil) + if err != nil { + return Response{}, errors.Wrap(err, errMakeRequestError) + } + var r Response + err = json.Unmarshal(res, &r) + if err != nil { + return Response{}, errors.Wrap(err, errUnmarshalError) + } + return r, nil +} + +// ListZones lists zones on an account. Optionally takes a list of zone names +// to filter against. +// +// API reference: https://api.cloudflare.com/#zone-list-zones +func (api *API) ListZones(z ...string) ([]Zone, error) { + v := url.Values{} + var res []byte + var r ZonesResponse + var zones []Zone + var err error + if len(z) > 0 { + for _, zone := range z { + v.Set("name", zone) + res, err = api.makeRequest("GET", "/zones?"+v.Encode(), nil) + if err != nil { + return []Zone{}, errors.Wrap(err, errMakeRequestError) + } + err = json.Unmarshal(res, &r) + if err != nil { + return []Zone{}, errors.Wrap(err, errUnmarshalError) + } + if !r.Success { + // TODO: Provide an actual error message instead of always returning nil + return []Zone{}, err + } + for zi := range r.Result { + zones = append(zones, r.Result[zi]) + } + } + } else { + // TODO: Paginate here. We only grab the first page of results. + // Could do this concurrently after the first request by creating a + // sync.WaitGroup or just a channel + workers. + res, err = api.makeRequest("GET", "/zones", nil) + if err != nil { + return []Zone{}, errors.Wrap(err, errMakeRequestError) + } + err = json.Unmarshal(res, &r) + if err != nil { + return []Zone{}, errors.Wrap(err, errUnmarshalError) + } + zones = r.Result + } + + return zones, nil +} + +// ZoneDetails fetches information about a zone. +// +// API reference: https://api.cloudflare.com/#zone-zone-details +func (api *API) ZoneDetails(zoneID string) (Zone, error) { + res, err := api.makeRequest("GET", "/zones/"+zoneID, nil) + if err != nil { + return Zone{}, errors.Wrap(err, errMakeRequestError) + } + var r ZoneResponse + err = json.Unmarshal(res, &r) + if err != nil { + return Zone{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// ZoneOptions is a subset of Zone, for editable options. +type ZoneOptions struct { + // FIXME(jamesog): Using omitempty here means we can't disable Paused. + // Currently unsure how to work around this. + Paused bool `json:"paused,omitempty"` + VanityNS []string `json:"vanity_name_servers,omitempty"` + Plan *ZonePlan `json:"plan,omitempty"` +} + +// ZoneSetPaused pauses Cloudflare service for the entire zone, sending all +// traffic direct to the origin. +func (api *API) ZoneSetPaused(zoneID string, paused bool) (Zone, error) { + zoneopts := ZoneOptions{Paused: paused} + zone, err := api.EditZone(zoneID, zoneopts) + if err != nil { + return Zone{}, err + } + + return zone, nil +} + +// ZoneSetVanityNS sets custom nameservers for the zone. +// These names must be within the same zone. +func (api *API) ZoneSetVanityNS(zoneID string, ns []string) (Zone, error) { + zoneopts := ZoneOptions{VanityNS: ns} + zone, err := api.EditZone(zoneID, zoneopts) + if err != nil { + return Zone{}, err + } + + return zone, nil +} + +// ZoneSetPlan changes the zone plan. +func (api *API) ZoneSetPlan(zoneID string, plan ZonePlan) (Zone, error) { + zoneopts := ZoneOptions{Plan: &plan} + zone, err := api.EditZone(zoneID, zoneopts) + if err != nil { + return Zone{}, err + } + + return zone, nil +} + +// EditZone edits the given zone. +// This is usually called by ZoneSetPaused, ZoneSetVanityNS or ZoneSetPlan. +// +// API reference: https://api.cloudflare.com/#zone-edit-zone-properties +func (api *API) EditZone(zoneID string, zoneOpts ZoneOptions) (Zone, error) { + res, err := api.makeRequest("PATCH", "/zones/"+zoneID, zoneOpts) + if err != nil { + return Zone{}, errors.Wrap(err, errMakeRequestError) + } + var r ZoneResponse + err = json.Unmarshal(res, &r) + if err != nil { + return Zone{}, errors.Wrap(err, errUnmarshalError) + } + + return r.Result, nil +} + +// PurgeEverything purges the cache for the given zone. +// Note: this will substantially increase load on the origin server for that +// zone if there is a high cached vs. uncached request ratio. +// +// API reference: https://api.cloudflare.com/#zone-purge-all-files +func (api *API) PurgeEverything(zoneID string) (PurgeCacheResponse, error) { + uri := "/zones/" + zoneID + "/purge_cache" + res, err := api.makeRequest("DELETE", uri, PurgeCacheRequest{true, nil, nil}) + if err != nil { + return PurgeCacheResponse{}, errors.Wrap(err, errMakeRequestError) + } + var r PurgeCacheResponse + err = json.Unmarshal(res, &r) + if err != nil { + return PurgeCacheResponse{}, errors.Wrap(err, errUnmarshalError) + } + return r, nil +} + +// PurgeCache purges the cache using the given PurgeCacheRequest (zone/url/tag). +// +// API reference: https://api.cloudflare.com/#zone-purge-individual-files-by-url-and-cache-tags +func (api *API) PurgeCache(zoneID string, pcr PurgeCacheRequest) (PurgeCacheResponse, error) { + uri := "/zones/" + zoneID + "/purge_cache" + res, err := api.makeRequest("DELETE", uri, pcr) + if err != nil { + return PurgeCacheResponse{}, errors.Wrap(err, errMakeRequestError) + } + var r PurgeCacheResponse + err = json.Unmarshal(res, &r) + if err != nil { + return PurgeCacheResponse{}, errors.Wrap(err, errUnmarshalError) + } + return r, nil +} + +// DeleteZone deletes the given zone. +// +// API reference: https://api.cloudflare.com/#zone-delete-a-zone +func (api *API) DeleteZone(zoneID string) (ZoneID, error) { + res, err := api.makeRequest("DELETE", "/zones/"+zoneID, nil) + if err != nil { + return ZoneID{}, errors.Wrap(err, errMakeRequestError) + } + var r ZoneIDResponse + err = json.Unmarshal(res, &r) + if err != nil { + return ZoneID{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// AvailableZonePlans returns information about all plans available to the specified zone. +// +// API reference: https://api.cloudflare.com/#zone-plan-available-plans +func (api *API) AvailableZonePlans(zoneID string) ([]ZonePlan, error) { + uri := "/zones/" + zoneID + "/available_plans" + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return []ZonePlan{}, errors.Wrap(err, errMakeRequestError) + } + var r AvailableZonePlansResponse + err = json.Unmarshal(res, &r) + if err != nil { + return []ZonePlan{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// ZonePlanDetails returns information about a zone plan. +// +// API reference: https://api.cloudflare.com/#zone-plan-plan-details +func (api *API) ZonePlanDetails(zoneID, planID string) (ZonePlan, error) { + uri := "/zones/" + zoneID + "/available_plans/" + planID + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return ZonePlan{}, errors.Wrap(err, errMakeRequestError) + } + var r ZonePlanResponse + err = json.Unmarshal(res, &r) + if err != nil { + return ZonePlan{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// encode encodes non-nil fields into URL encoded form. +func (o ZoneAnalyticsOptions) encode() string { + v := url.Values{} + if o.Since != nil { + v.Set("since", (*o.Since).Format(time.RFC3339)) + } + if o.Until != nil { + v.Set("until", (*o.Until).Format(time.RFC3339)) + } + if o.Continuous != nil { + v.Set("continuous", fmt.Sprintf("%t", *o.Continuous)) + } + return v.Encode() +} + +// ZoneAnalyticsDashboard returns zone analytics information. +// +// API reference: +// https://api.cloudflare.com/#zone-analytics-dashboard +// GET /zones/:zone_identifier/analytics/dashboard +func (api *API) ZoneAnalyticsDashboard(zoneID string, options ZoneAnalyticsOptions) (ZoneAnalyticsData, error) { + uri := "/zones/" + zoneID + "/analytics/dashboard" + "?" + options.encode() + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return ZoneAnalyticsData{}, errors.Wrap(err, errMakeRequestError) + } + var r zoneAnalyticsDataResponse + err = json.Unmarshal(res, &r) + if err != nil { + return ZoneAnalyticsData{}, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// ZoneAnalyticsByColocation returns zone analytics information by datacenter. +// +// API reference: +// https://api.cloudflare.com/#zone-analytics-analytics-by-co-locations +// GET /zones/:zone_identifier/analytics/colos +func (api *API) ZoneAnalyticsByColocation(zoneID string, options ZoneAnalyticsOptions) ([]ZoneAnalyticsColocation, error) { + uri := "/zones/" + zoneID + "/analytics/colos" + "?" + options.encode() + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return nil, errors.Wrap(err, errMakeRequestError) + } + var r zoneAnalyticsColocationResponse + err = json.Unmarshal(res, &r) + if err != nil { + return nil, errors.Wrap(err, errUnmarshalError) + } + return r.Result, nil +} + +// Zone Settings +// https://api.cloudflare.com/#zone-settings-for-a-zone-get-all-zone-settings +// e.g. +// https://api.cloudflare.com/#zone-settings-for-a-zone-get-always-online-setting +// https://api.cloudflare.com/#zone-settings-for-a-zone-change-always-online-setting diff --git a/vendor/github.com/cloudflare/cloudflare-go/zone_test.go b/vendor/github.com/cloudflare/cloudflare-go/zone_test.go new file mode 100644 index 000000000..b96081cb3 --- /dev/null +++ b/vendor/github.com/cloudflare/cloudflare-go/zone_test.go @@ -0,0 +1,584 @@ +package cloudflare + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestZoneAnalyticsDashboard(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method, "Expected method 'GET', got %s", r.Method) + assert.Equal(t, "2015-01-01T12:23:00Z", r.URL.Query().Get("since")) + assert.Equal(t, "2015-01-02T12:23:00Z", r.URL.Query().Get("until")) + assert.Equal(t, "true", r.URL.Query().Get("continuous")) + + w.Header().Set("content-type", "application/json") + // JSON data from: https://api.cloudflare.com/#zone-analytics-properties + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "totals": { + "since": "2015-01-01T12:23:00Z", + "until": "2015-01-02T12:23:00Z", + "requests": { + "all": 1234085328, + "cached": 1234085328, + "uncached": 13876154, + "content_type": { + "css": 15343, + "html": 1234213, + "javascript": 318236, + "gif": 23178, + "jpeg": 1982048 + }, + "country": { + "US": 4181364, + "AG": 37298, + "GI": 293846 + }, + "ssl": { + "encrypted": 12978361, + "unencrypted": 781263 + }, + "http_status": { + "200": 13496983, + "301": 283, + "400": 187936, + "402": 1828, + "404": 1293 + } + }, + "bandwidth": { + "all": 213867451, + "cached": 113205063, + "uncached": 113205063, + "content_type": { + "css": 237421, + "html": 1231290, + "javascript": 123245, + "gif": 1234242, + "jpeg": 784278 + }, + "country": { + "US": 123145433, + "AG": 2342483, + "GI": 984753 + }, + "ssl": { + "encrypted": 37592942, + "unencrypted": 237654192 + } + }, + "threats": { + "all": 23423873, + "country": { + "US": 123, + "CN": 523423, + "AU": 91 + }, + "type": { + "user.ban.ip": 123, + "hot.ban.unknown": 5324, + "macro.chl.captchaErr": 1341, + "macro.chl.jschlErr": 5323 + } + }, + "pageviews": { + "all": 5724723, + "search_engines": { + "googlebot": 35272, + "pingdom": 13435, + "bingbot": 5372, + "baidubot": 1345 + } + }, + "uniques": { + "all": 12343 + } + }, + "timeseries": [ + { + "since": "2015-01-01T12:23:00Z", + "until": "2015-01-02T12:23:00Z", + "requests": { + "all": 1234085328, + "cached": 1234085328, + "uncached": 13876154, + "content_type": { + "css": 15343, + "html": 1234213, + "javascript": 318236, + "gif": 23178, + "jpeg": 1982048 + }, + "country": { + "US": 4181364, + "AG": 37298, + "GI": 293846 + }, + "ssl": { + "encrypted": 12978361, + "unencrypted": 781263 + }, + "http_status": { + "200": 13496983, + "301": 283, + "400": 187936, + "402": 1828, + "404": 1293 + } + }, + "bandwidth": { + "all": 213867451, + "cached": 113205063, + "uncached": 113205063, + "content_type": { + "css": 237421, + "html": 1231290, + "javascript": 123245, + "gif": 1234242, + "jpeg": 784278 + }, + "country": { + "US": 123145433, + "AG": 2342483, + "GI": 984753 + }, + "ssl": { + "encrypted": 37592942, + "unencrypted": 237654192 + } + }, + "threats": { + "all": 23423873, + "country": { + "US": 123, + "CN": 523423, + "AU": 91 + }, + "type": { + "user.ban.ip": 123, + "hot.ban.unknown": 5324, + "macro.chl.captchaErr": 1341, + "macro.chl.jschlErr": 5323 + } + }, + "pageviews": { + "all": 5724723, + "search_engines": { + "googlebot": 35272, + "pingdom": 13435, + "bingbot": 5372, + "baidubot": 1345 + } + }, + "uniques": { + "all": 12343 + } + } + ] + }, + "query": { + "since": "2015-01-01T12:23:00Z", + "until": "2015-01-02T12:23:00Z", + "time_delta": 60 + } + }`) + } + + mux.HandleFunc("/zones/foo/analytics/dashboard", handler) + + since, _ := time.Parse(time.RFC3339, "2015-01-01T12:23:00Z") + until, _ := time.Parse(time.RFC3339, "2015-01-02T12:23:00Z") + data := ZoneAnalytics{ + Since: since, + Until: until, + Requests: struct { + All int `json:"all"` + Cached int `json:"cached"` + Uncached int `json:"uncached"` + ContentType map[string]int `json:"content_type"` + Country map[string]int `json:"country"` + SSL struct { + Encrypted int `json:"encrypted"` + Unencrypted int `json:"unencrypted"` + } `json:"ssl"` + HTTPStatus map[string]int `json:"http_status"` + }{ + All: 1234085328, + Cached: 1234085328, + Uncached: 13876154, + ContentType: map[string]int{ + "css": 15343, + "html": 1234213, + "javascript": 318236, + "gif": 23178, + "jpeg": 1982048, + }, + Country: map[string]int{ + "US": 4181364, + "AG": 37298, + "GI": 293846, + }, + SSL: struct { + Encrypted int `json:"encrypted"` + Unencrypted int `json:"unencrypted"` + }{ + Encrypted: 12978361, + Unencrypted: 781263, + }, + HTTPStatus: map[string]int{ + "200": 13496983, + "301": 283, + "400": 187936, + "402": 1828, + "404": 1293, + }, + }, + Bandwidth: struct { + All int `json:"all"` + Cached int `json:"cached"` + Uncached int `json:"uncached"` + ContentType map[string]int `json:"content_type"` + Country map[string]int `json:"country"` + SSL struct { + Encrypted int `json:"encrypted"` + Unencrypted int `json:"unencrypted"` + } `json:"ssl"` + }{ + All: 213867451, + Cached: 113205063, + Uncached: 113205063, + ContentType: map[string]int{ + "css": 237421, + "html": 1231290, + "javascript": 123245, + "gif": 1234242, + "jpeg": 784278, + }, + Country: map[string]int{ + "US": 123145433, + "AG": 2342483, + "GI": 984753, + }, + SSL: struct { + Encrypted int `json:"encrypted"` + Unencrypted int `json:"unencrypted"` + }{ + Encrypted: 37592942, + Unencrypted: 237654192, + }, + }, + Threats: struct { + All int `json:"all"` + Country map[string]int `json:"country"` + Type map[string]int `json:"type"` + }{ + All: 23423873, + Country: map[string]int{ + "US": 123, + "CN": 523423, + "AU": 91, + }, + Type: map[string]int{ + "user.ban.ip": 123, + "hot.ban.unknown": 5324, + "macro.chl.captchaErr": 1341, + "macro.chl.jschlErr": 5323, + }, + }, + Pageviews: struct { + All int `json:"all"` + SearchEngines map[string]int `json:"search_engines"` + }{ + All: 5724723, + SearchEngines: map[string]int{ + "googlebot": 35272, + "pingdom": 13435, + "bingbot": 5372, + "baidubot": 1345, + }, + }, + Uniques: struct { + All int `json:"all"` + }{ + All: 12343, + }, + } + want := ZoneAnalyticsData{ + Totals: data, + Timeseries: []ZoneAnalytics{data}, + } + + continuous := true + d, err := client.ZoneAnalyticsDashboard("foo", ZoneAnalyticsOptions{ + Since: &since, + Until: &until, + Continuous: &continuous, + }) + if assert.NoError(t, err) { + assert.Equal(t, want, d) + } + + _, err = client.ZoneAnalyticsDashboard("bar", ZoneAnalyticsOptions{}) + assert.Error(t, err) +} + +func TestZoneAnalyticsByColocation(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method, "Expected method 'GET', got %s", r.Method) + assert.Equal(t, "2015-01-01T12:23:00Z", r.URL.Query().Get("since")) + assert.Equal(t, "2015-01-02T12:23:00Z", r.URL.Query().Get("until")) + assert.Equal(t, "true", r.URL.Query().Get("continuous")) + + w.Header().Set("content-type", "application/json") + // JSON data from: https://api.cloudflare.com/#zone-analytics-analytics-by-co-locations + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "colo_id": "SFO", + "timeseries": [ + { + "since": "2015-01-01T12:23:00Z", + "until": "2015-01-02T12:23:00Z", + "requests": { + "all": 1234085328, + "cached": 1234085328, + "uncached": 13876154, + "content_type": { + "css": 15343, + "html": 1234213, + "javascript": 318236, + "gif": 23178, + "jpeg": 1982048 + }, + "country": { + "US": 4181364, + "AG": 37298, + "GI": 293846 + }, + "ssl": { + "encrypted": 12978361, + "unencrypted": 781263 + }, + "http_status": { + "200": 13496983, + "301": 283, + "400": 187936, + "402": 1828, + "404": 1293 + } + }, + "bandwidth": { + "all": 213867451, + "cached": 113205063, + "uncached": 113205063, + "content_type": { + "css": 237421, + "html": 1231290, + "javascript": 123245, + "gif": 1234242, + "jpeg": 784278 + }, + "country": { + "US": 123145433, + "AG": 2342483, + "GI": 984753 + }, + "ssl": { + "encrypted": 37592942, + "unencrypted": 237654192 + } + }, + "threats": { + "all": 23423873, + "country": { + "US": 123, + "CN": 523423, + "AU": 91 + }, + "type": { + "user.ban.ip": 123, + "hot.ban.unknown": 5324, + "macro.chl.captchaErr": 1341, + "macro.chl.jschlErr": 5323 + } + }, + "pageviews": { + "all": 5724723, + "search_engines": { + "googlebot": 35272, + "pingdom": 13435, + "bingbot": 5372, + "baidubot": 1345 + } + }, + "uniques": { + "all": 12343 + } + } + ] + } + ], + "query": { + "since": "2015-01-01T12:23:00Z", + "until": "2015-01-02T12:23:00Z", + "time_delta": 60 + } + }`) + } + + mux.HandleFunc("/zones/foo/analytics/colos", handler) + + since, _ := time.Parse(time.RFC3339, "2015-01-01T12:23:00Z") + until, _ := time.Parse(time.RFC3339, "2015-01-02T12:23:00Z") + data := ZoneAnalytics{ + Since: since, + Until: until, + Requests: struct { + All int `json:"all"` + Cached int `json:"cached"` + Uncached int `json:"uncached"` + ContentType map[string]int `json:"content_type"` + Country map[string]int `json:"country"` + SSL struct { + Encrypted int `json:"encrypted"` + Unencrypted int `json:"unencrypted"` + } `json:"ssl"` + HTTPStatus map[string]int `json:"http_status"` + }{ + All: 1234085328, + Cached: 1234085328, + Uncached: 13876154, + ContentType: map[string]int{ + "css": 15343, + "html": 1234213, + "javascript": 318236, + "gif": 23178, + "jpeg": 1982048, + }, + Country: map[string]int{ + "US": 4181364, + "AG": 37298, + "GI": 293846, + }, + SSL: struct { + Encrypted int `json:"encrypted"` + Unencrypted int `json:"unencrypted"` + }{ + Encrypted: 12978361, + Unencrypted: 781263, + }, + HTTPStatus: map[string]int{ + "200": 13496983, + "301": 283, + "400": 187936, + "402": 1828, + "404": 1293, + }, + }, + Bandwidth: struct { + All int `json:"all"` + Cached int `json:"cached"` + Uncached int `json:"uncached"` + ContentType map[string]int `json:"content_type"` + Country map[string]int `json:"country"` + SSL struct { + Encrypted int `json:"encrypted"` + Unencrypted int `json:"unencrypted"` + } `json:"ssl"` + }{ + All: 213867451, + Cached: 113205063, + Uncached: 113205063, + ContentType: map[string]int{ + "css": 237421, + "html": 1231290, + "javascript": 123245, + "gif": 1234242, + "jpeg": 784278, + }, + Country: map[string]int{ + "US": 123145433, + "AG": 2342483, + "GI": 984753, + }, + SSL: struct { + Encrypted int `json:"encrypted"` + Unencrypted int `json:"unencrypted"` + }{ + Encrypted: 37592942, + Unencrypted: 237654192, + }, + }, + Threats: struct { + All int `json:"all"` + Country map[string]int `json:"country"` + Type map[string]int `json:"type"` + }{ + All: 23423873, + Country: map[string]int{ + "US": 123, + "CN": 523423, + "AU": 91, + }, + Type: map[string]int{ + "user.ban.ip": 123, + "hot.ban.unknown": 5324, + "macro.chl.captchaErr": 1341, + "macro.chl.jschlErr": 5323, + }, + }, + Pageviews: struct { + All int `json:"all"` + SearchEngines map[string]int `json:"search_engines"` + }{ + All: 5724723, + SearchEngines: map[string]int{ + "googlebot": 35272, + "pingdom": 13435, + "bingbot": 5372, + "baidubot": 1345, + }, + }, + Uniques: struct { + All int `json:"all"` + }{ + All: 12343, + }, + } + want := []ZoneAnalyticsColocation{ + { + ColocationID: "SFO", + Timeseries: []ZoneAnalytics{data}, + }, + } + + continuous := true + d, err := client.ZoneAnalyticsByColocation("foo", ZoneAnalyticsOptions{ + Since: &since, + Until: &until, + Continuous: &continuous, + }) + if assert.NoError(t, err) { + assert.Equal(t, want, d) + } + + _, err = client.ZoneAnalyticsDashboard("bar", ZoneAnalyticsOptions{}) + assert.Error(t, err) +} diff --git a/vendor/github.com/pkg/errors/.gitignore b/vendor/github.com/pkg/errors/.gitignore new file mode 100644 index 000000000..daf913b1b --- /dev/null +++ b/vendor/github.com/pkg/errors/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/pkg/errors/.travis.yml b/vendor/github.com/pkg/errors/.travis.yml new file mode 100644 index 000000000..588ceca18 --- /dev/null +++ b/vendor/github.com/pkg/errors/.travis.yml @@ -0,0 +1,11 @@ +language: go +go_import_path: github.com/pkg/errors +go: + - 1.4.3 + - 1.5.4 + - 1.6.2 + - 1.7.1 + - tip + +script: + - go test -v ./... diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE new file mode 100644 index 000000000..835ba3e75 --- /dev/null +++ b/vendor/github.com/pkg/errors/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Dave Cheney +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pkg/errors/README.md b/vendor/github.com/pkg/errors/README.md new file mode 100644 index 000000000..273db3c98 --- /dev/null +++ b/vendor/github.com/pkg/errors/README.md @@ -0,0 +1,52 @@ +# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) + +Package errors provides simple error handling primitives. + +`go get github.com/pkg/errors` + +The traditional error handling idiom in Go is roughly akin to +```go +if err != nil { + return err +} +``` +which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error. + +## Adding context to an error + +The errors.Wrap function returns a new error that adds context to the original error. For example +```go +_, err := ioutil.ReadAll(r) +if err != nil { + return errors.Wrap(err, "read failed") +} +``` +## Retrieving the cause of an error + +Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`. +```go +type causer interface { + Cause() error +} +``` +`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example: +```go +switch err := errors.Cause(err).(type) { +case *MyError: + // handle specifically +default: + // unknown error +} +``` + +[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors). + +## Contributing + +We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high. + +Before proposing a change, please discuss your change by raising an issue. + +## Licence + +BSD-2-Clause diff --git a/vendor/github.com/pkg/errors/appveyor.yml b/vendor/github.com/pkg/errors/appveyor.yml new file mode 100644 index 000000000..a932eade0 --- /dev/null +++ b/vendor/github.com/pkg/errors/appveyor.yml @@ -0,0 +1,32 @@ +version: build-{build}.{branch} + +clone_folder: C:\gopath\src\github.com\pkg\errors +shallow_clone: true # for startup speed + +environment: + GOPATH: C:\gopath + +platform: + - x64 + +# http://www.appveyor.com/docs/installed-software +install: + # some helpful output for debugging builds + - go version + - go env + # pre-installed MinGW at C:\MinGW is 32bit only + # but MSYS2 at C:\msys64 has mingw64 + - set PATH=C:\msys64\mingw64\bin;%PATH% + - gcc --version + - g++ --version + +build_script: + - go install -v ./... + +test_script: + - set PATH=C:\gopath\bin;%PATH% + - go test -v ./... + +#artifacts: +# - path: '%GOPATH%\bin\*.exe' +deploy: off diff --git a/vendor/github.com/pkg/errors/bench_test.go b/vendor/github.com/pkg/errors/bench_test.go new file mode 100644 index 000000000..0416a3cbb --- /dev/null +++ b/vendor/github.com/pkg/errors/bench_test.go @@ -0,0 +1,59 @@ +// +build go1.7 + +package errors + +import ( + "fmt" + "testing" + + stderrors "errors" +) + +func noErrors(at, depth int) error { + if at >= depth { + return stderrors.New("no error") + } + return noErrors(at+1, depth) +} +func yesErrors(at, depth int) error { + if at >= depth { + return New("ye error") + } + return yesErrors(at+1, depth) +} + +func BenchmarkErrors(b *testing.B) { + var toperr error + type run struct { + stack int + std bool + } + runs := []run{ + {10, false}, + {10, true}, + {100, false}, + {100, true}, + {1000, false}, + {1000, true}, + } + for _, r := range runs { + part := "pkg/errors" + if r.std { + part = "errors" + } + name := fmt.Sprintf("%s-stack-%d", part, r.stack) + b.Run(name, func(b *testing.B) { + var err error + f := yesErrors + if r.std { + f = noErrors + } + b.ReportAllocs() + for i := 0; i < b.N; i++ { + err = f(0, r.stack) + } + b.StopTimer() + toperr = err + }) + } +} diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go new file mode 100644 index 000000000..842ee8045 --- /dev/null +++ b/vendor/github.com/pkg/errors/errors.go @@ -0,0 +1,269 @@ +// Package errors provides simple error handling primitives. +// +// The traditional error handling idiom in Go is roughly akin to +// +// if err != nil { +// return err +// } +// +// which applied recursively up the call stack results in error reports +// without context or debugging information. The errors package allows +// programmers to add context to the failure path in their code in a way +// that does not destroy the original value of the error. +// +// Adding context to an error +// +// The errors.Wrap function returns a new error that adds context to the +// original error by recording a stack trace at the point Wrap is called, +// and the supplied message. For example +// +// _, err := ioutil.ReadAll(r) +// if err != nil { +// return errors.Wrap(err, "read failed") +// } +// +// If additional control is required the errors.WithStack and errors.WithMessage +// functions destructure errors.Wrap into its component operations of annotating +// an error with a stack trace and an a message, respectively. +// +// Retrieving the cause of an error +// +// Using errors.Wrap constructs a stack of errors, adding context to the +// preceding error. Depending on the nature of the error it may be necessary +// to reverse the operation of errors.Wrap to retrieve the original error +// for inspection. Any error value which implements this interface +// +// type causer interface { +// Cause() error +// } +// +// can be inspected by errors.Cause. errors.Cause will recursively retrieve +// the topmost error which does not implement causer, which is assumed to be +// the original cause. For example: +// +// switch err := errors.Cause(err).(type) { +// case *MyError: +// // handle specifically +// default: +// // unknown error +// } +// +// causer interface is not exported by this package, but is considered a part +// of stable public API. +// +// Formatted printing of errors +// +// All error values returned from this package implement fmt.Formatter and can +// be formatted by the fmt package. The following verbs are supported +// +// %s print the error. If the error has a Cause it will be +// printed recursively +// %v see %s +// %+v extended format. Each Frame of the error's StackTrace will +// be printed in detail. +// +// Retrieving the stack trace of an error or wrapper +// +// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are +// invoked. This information can be retrieved with the following interface. +// +// type stackTracer interface { +// StackTrace() errors.StackTrace +// } +// +// Where errors.StackTrace is defined as +// +// type StackTrace []Frame +// +// The Frame type represents a call site in the stack trace. Frame supports +// the fmt.Formatter interface that can be used for printing information about +// the stack trace of this error. For example: +// +// if err, ok := err.(stackTracer); ok { +// for _, f := range err.StackTrace() { +// fmt.Printf("%+s:%d", f) +// } +// } +// +// stackTracer interface is not exported by this package, but is considered a part +// of stable public API. +// +// See the documentation for Frame.Format for more details. +package errors + +import ( + "fmt" + "io" +) + +// New returns an error with the supplied message. +// New also records the stack trace at the point it was called. +func New(message string) error { + return &fundamental{ + msg: message, + stack: callers(), + } +} + +// Errorf formats according to a format specifier and returns the string +// as a value that satisfies error. +// Errorf also records the stack trace at the point it was called. +func Errorf(format string, args ...interface{}) error { + return &fundamental{ + msg: fmt.Sprintf(format, args...), + stack: callers(), + } +} + +// fundamental is an error that has a message and a stack, but no caller. +type fundamental struct { + msg string + *stack +} + +func (f *fundamental) Error() string { return f.msg } + +func (f *fundamental) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, f.msg) + f.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, f.msg) + case 'q': + fmt.Fprintf(s, "%q", f.msg) + } +} + +// WithStack annotates err with a stack trace at the point WithStack was called. +// If err is nil, WithStack returns nil. +func WithStack(err error) error { + if err == nil { + return nil + } + return &withStack{ + err, + callers(), + } +} + +type withStack struct { + error + *stack +} + +func (w *withStack) Cause() error { return w.error } + +func (w *withStack) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v", w.Cause()) + w.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, w.Error()) + case 'q': + fmt.Fprintf(s, "%q", w.Error()) + } +} + +// Wrap returns an error annotating err with a stack trace +// at the point Wrap is called, and the supplied message. +// If err is nil, Wrap returns nil. +func Wrap(err error, message string) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: message, + } + return &withStack{ + err, + callers(), + } +} + +// Wrapf returns an error annotating err with a stack trace +// at the point Wrapf is call, and the format specifier. +// If err is nil, Wrapf returns nil. +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } + return &withStack{ + err, + callers(), + } +} + +// WithMessage annotates err with a new message. +// If err is nil, WithMessage returns nil. +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: message, + } +} + +type withMessage struct { + cause error + msg string +} + +func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } +func (w *withMessage) Cause() error { return w.cause } + +func (w *withMessage) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v\n", w.Cause()) + io.WriteString(s, w.msg) + return + } + fallthrough + case 's', 'q': + io.WriteString(s, w.Error()) + } +} + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return err +} diff --git a/vendor/github.com/pkg/errors/errors_test.go b/vendor/github.com/pkg/errors/errors_test.go new file mode 100644 index 000000000..1d8c63558 --- /dev/null +++ b/vendor/github.com/pkg/errors/errors_test.go @@ -0,0 +1,226 @@ +package errors + +import ( + "errors" + "fmt" + "io" + "reflect" + "testing" +) + +func TestNew(t *testing.T) { + tests := []struct { + err string + want error + }{ + {"", fmt.Errorf("")}, + {"foo", fmt.Errorf("foo")}, + {"foo", New("foo")}, + {"string with format specifiers: %v", errors.New("string with format specifiers: %v")}, + } + + for _, tt := range tests { + got := New(tt.err) + if got.Error() != tt.want.Error() { + t.Errorf("New.Error(): got: %q, want %q", got, tt.want) + } + } +} + +func TestWrapNil(t *testing.T) { + got := Wrap(nil, "no error") + if got != nil { + t.Errorf("Wrap(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWrap(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {Wrap(io.EOF, "read error"), "client error", "client error: read error: EOF"}, + } + + for _, tt := range tests { + got := Wrap(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("Wrap(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want) + } + } +} + +type nilError struct{} + +func (nilError) Error() string { return "nil error" } + +func TestCause(t *testing.T) { + x := New("error") + tests := []struct { + err error + want error + }{{ + // nil error is nil + err: nil, + want: nil, + }, { + // explicit nil error is nil + err: (error)(nil), + want: nil, + }, { + // typed nil is nil + err: (*nilError)(nil), + want: (*nilError)(nil), + }, { + // uncaused error is unaffected + err: io.EOF, + want: io.EOF, + }, { + // caused error returns cause + err: Wrap(io.EOF, "ignored"), + want: io.EOF, + }, { + err: x, // return from errors.New + want: x, + }, { + WithMessage(nil, "whoops"), + nil, + }, { + WithMessage(io.EOF, "whoops"), + io.EOF, + }, { + WithStack(nil), + nil, + }, { + WithStack(io.EOF), + io.EOF, + }} + + for i, tt := range tests { + got := Cause(tt.err) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("test %d: got %#v, want %#v", i+1, got, tt.want) + } + } +} + +func TestWrapfNil(t *testing.T) { + got := Wrapf(nil, "no error") + if got != nil { + t.Errorf("Wrapf(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWrapf(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {Wrapf(io.EOF, "read error without format specifiers"), "client error", "client error: read error without format specifiers: EOF"}, + {Wrapf(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"}, + } + + for _, tt := range tests { + got := Wrapf(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("Wrapf(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want) + } + } +} + +func TestErrorf(t *testing.T) { + tests := []struct { + err error + want string + }{ + {Errorf("read error without format specifiers"), "read error without format specifiers"}, + {Errorf("read error with %d format specifier", 1), "read error with 1 format specifier"}, + } + + for _, tt := range tests { + got := tt.err.Error() + if got != tt.want { + t.Errorf("Errorf(%v): got: %q, want %q", tt.err, got, tt.want) + } + } +} + +func TestWithStackNil(t *testing.T) { + got := WithStack(nil) + if got != nil { + t.Errorf("WithStack(nil): got %#v, expected nil", got) + } +} + +func TestWithStack(t *testing.T) { + tests := []struct { + err error + want string + }{ + {io.EOF, "EOF"}, + {WithStack(io.EOF), "EOF"}, + } + + for _, tt := range tests { + got := WithStack(tt.err).Error() + if got != tt.want { + t.Errorf("WithStack(%v): got: %v, want %v", tt.err, got, tt.want) + } + } +} + +func TestWithMessageNil(t *testing.T) { + got := WithMessage(nil, "no error") + if got != nil { + t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWithMessage(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {WithMessage(io.EOF, "read error"), "client error", "client error: read error: EOF"}, + } + + for _, tt := range tests { + got := WithMessage(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want) + } + } + +} + +// errors.New, etc values are not expected to be compared by value +// but the change in errors#27 made them incomparable. Assert that +// various kinds of errors have a functional equality operator, even +// if the result of that equality is always false. +func TestErrorEquality(t *testing.T) { + vals := []error{ + nil, + io.EOF, + errors.New("EOF"), + New("EOF"), + Errorf("EOF"), + Wrap(io.EOF, "EOF"), + Wrapf(io.EOF, "EOF%d", 2), + WithMessage(nil, "whoops"), + WithMessage(io.EOF, "whoops"), + WithStack(io.EOF), + WithStack(nil), + } + + for i := range vals { + for j := range vals { + _ = vals[i] == vals[j] // mustn't panic + } + } +} diff --git a/vendor/github.com/pkg/errors/example_test.go b/vendor/github.com/pkg/errors/example_test.go new file mode 100644 index 000000000..c1fc13e38 --- /dev/null +++ b/vendor/github.com/pkg/errors/example_test.go @@ -0,0 +1,205 @@ +package errors_test + +import ( + "fmt" + + "github.com/pkg/errors" +) + +func ExampleNew() { + err := errors.New("whoops") + fmt.Println(err) + + // Output: whoops +} + +func ExampleNew_printf() { + err := errors.New("whoops") + fmt.Printf("%+v", err) + + // Example output: + // whoops + // github.com/pkg/errors_test.ExampleNew_printf + // /home/dfc/src/github.com/pkg/errors/example_test.go:17 + // testing.runExample + // /home/dfc/go/src/testing/example.go:114 + // testing.RunExamples + // /home/dfc/go/src/testing/example.go:38 + // testing.(*M).Run + // /home/dfc/go/src/testing/testing.go:744 + // main.main + // /github.com/pkg/errors/_test/_testmain.go:106 + // runtime.main + // /home/dfc/go/src/runtime/proc.go:183 + // runtime.goexit + // /home/dfc/go/src/runtime/asm_amd64.s:2059 +} + +func ExampleWithMessage() { + cause := errors.New("whoops") + err := errors.WithMessage(cause, "oh noes") + fmt.Println(err) + + // Output: oh noes: whoops +} + +func ExampleWithStack() { + cause := errors.New("whoops") + err := errors.WithStack(cause) + fmt.Println(err) + + // Output: whoops +} + +func ExampleWithStack_printf() { + cause := errors.New("whoops") + err := errors.WithStack(cause) + fmt.Printf("%+v", err) + + // Example Output: + // whoops + // github.com/pkg/errors_test.ExampleWithStack_printf + // /home/fabstu/go/src/github.com/pkg/errors/example_test.go:55 + // testing.runExample + // /usr/lib/go/src/testing/example.go:114 + // testing.RunExamples + // /usr/lib/go/src/testing/example.go:38 + // testing.(*M).Run + // /usr/lib/go/src/testing/testing.go:744 + // main.main + // github.com/pkg/errors/_test/_testmain.go:106 + // runtime.main + // /usr/lib/go/src/runtime/proc.go:183 + // runtime.goexit + // /usr/lib/go/src/runtime/asm_amd64.s:2086 + // github.com/pkg/errors_test.ExampleWithStack_printf + // /home/fabstu/go/src/github.com/pkg/errors/example_test.go:56 + // testing.runExample + // /usr/lib/go/src/testing/example.go:114 + // testing.RunExamples + // /usr/lib/go/src/testing/example.go:38 + // testing.(*M).Run + // /usr/lib/go/src/testing/testing.go:744 + // main.main + // github.com/pkg/errors/_test/_testmain.go:106 + // runtime.main + // /usr/lib/go/src/runtime/proc.go:183 + // runtime.goexit + // /usr/lib/go/src/runtime/asm_amd64.s:2086 +} + +func ExampleWrap() { + cause := errors.New("whoops") + err := errors.Wrap(cause, "oh noes") + fmt.Println(err) + + // Output: oh noes: whoops +} + +func fn() error { + e1 := errors.New("error") + e2 := errors.Wrap(e1, "inner") + e3 := errors.Wrap(e2, "middle") + return errors.Wrap(e3, "outer") +} + +func ExampleCause() { + err := fn() + fmt.Println(err) + fmt.Println(errors.Cause(err)) + + // Output: outer: middle: inner: error + // error +} + +func ExampleWrap_extended() { + err := fn() + fmt.Printf("%+v\n", err) + + // Example output: + // error + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:47 + // github.com/pkg/errors_test.ExampleCause_printf + // /home/dfc/src/github.com/pkg/errors/example_test.go:63 + // testing.runExample + // /home/dfc/go/src/testing/example.go:114 + // testing.RunExamples + // /home/dfc/go/src/testing/example.go:38 + // testing.(*M).Run + // /home/dfc/go/src/testing/testing.go:744 + // main.main + // /github.com/pkg/errors/_test/_testmain.go:104 + // runtime.main + // /home/dfc/go/src/runtime/proc.go:183 + // runtime.goexit + // /home/dfc/go/src/runtime/asm_amd64.s:2059 + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:48: inner + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:49: middle + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:50: outer +} + +func ExampleWrapf() { + cause := errors.New("whoops") + err := errors.Wrapf(cause, "oh noes #%d", 2) + fmt.Println(err) + + // Output: oh noes #2: whoops +} + +func ExampleErrorf_extended() { + err := errors.Errorf("whoops: %s", "foo") + fmt.Printf("%+v", err) + + // Example output: + // whoops: foo + // github.com/pkg/errors_test.ExampleErrorf + // /home/dfc/src/github.com/pkg/errors/example_test.go:101 + // testing.runExample + // /home/dfc/go/src/testing/example.go:114 + // testing.RunExamples + // /home/dfc/go/src/testing/example.go:38 + // testing.(*M).Run + // /home/dfc/go/src/testing/testing.go:744 + // main.main + // /github.com/pkg/errors/_test/_testmain.go:102 + // runtime.main + // /home/dfc/go/src/runtime/proc.go:183 + // runtime.goexit + // /home/dfc/go/src/runtime/asm_amd64.s:2059 +} + +func Example_stackTrace() { + type stackTracer interface { + StackTrace() errors.StackTrace + } + + err, ok := errors.Cause(fn()).(stackTracer) + if !ok { + panic("oops, err does not implement stackTracer") + } + + st := err.StackTrace() + fmt.Printf("%+v", st[0:2]) // top two frames + + // Example output: + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:47 + // github.com/pkg/errors_test.Example_stackTrace + // /home/dfc/src/github.com/pkg/errors/example_test.go:127 +} + +func ExampleCause_printf() { + err := errors.Wrap(func() error { + return func() error { + return errors.Errorf("hello %s", fmt.Sprintf("world")) + }() + }(), "failed") + + fmt.Printf("%v", err) + + // Output: failed: hello world +} diff --git a/vendor/github.com/pkg/errors/format_test.go b/vendor/github.com/pkg/errors/format_test.go new file mode 100644 index 000000000..15fd7d89d --- /dev/null +++ b/vendor/github.com/pkg/errors/format_test.go @@ -0,0 +1,535 @@ +package errors + +import ( + "errors" + "fmt" + "io" + "regexp" + "strings" + "testing" +) + +func TestFormatNew(t *testing.T) { + tests := []struct { + error + format string + want string + }{{ + New("error"), + "%s", + "error", + }, { + New("error"), + "%v", + "error", + }, { + New("error"), + "%+v", + "error\n" + + "github.com/pkg/errors.TestFormatNew\n" + + "\t.+/github.com/pkg/errors/format_test.go:26", + }, { + New("error"), + "%q", + `"error"`, + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.error, tt.format, tt.want) + } +} + +func TestFormatErrorf(t *testing.T) { + tests := []struct { + error + format string + want string + }{{ + Errorf("%s", "error"), + "%s", + "error", + }, { + Errorf("%s", "error"), + "%v", + "error", + }, { + Errorf("%s", "error"), + "%+v", + "error\n" + + "github.com/pkg/errors.TestFormatErrorf\n" + + "\t.+/github.com/pkg/errors/format_test.go:56", + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.error, tt.format, tt.want) + } +} + +func TestFormatWrap(t *testing.T) { + tests := []struct { + error + format string + want string + }{{ + Wrap(New("error"), "error2"), + "%s", + "error2: error", + }, { + Wrap(New("error"), "error2"), + "%v", + "error2: error", + }, { + Wrap(New("error"), "error2"), + "%+v", + "error\n" + + "github.com/pkg/errors.TestFormatWrap\n" + + "\t.+/github.com/pkg/errors/format_test.go:82", + }, { + Wrap(io.EOF, "error"), + "%s", + "error: EOF", + }, { + Wrap(io.EOF, "error"), + "%v", + "error: EOF", + }, { + Wrap(io.EOF, "error"), + "%+v", + "EOF\n" + + "error\n" + + "github.com/pkg/errors.TestFormatWrap\n" + + "\t.+/github.com/pkg/errors/format_test.go:96", + }, { + Wrap(Wrap(io.EOF, "error1"), "error2"), + "%+v", + "EOF\n" + + "error1\n" + + "github.com/pkg/errors.TestFormatWrap\n" + + "\t.+/github.com/pkg/errors/format_test.go:103\n", + }, { + Wrap(New("error with space"), "context"), + "%q", + `"context: error with space"`, + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.error, tt.format, tt.want) + } +} + +func TestFormatWrapf(t *testing.T) { + tests := []struct { + error + format string + want string + }{{ + Wrapf(io.EOF, "error%d", 2), + "%s", + "error2: EOF", + }, { + Wrapf(io.EOF, "error%d", 2), + "%v", + "error2: EOF", + }, { + Wrapf(io.EOF, "error%d", 2), + "%+v", + "EOF\n" + + "error2\n" + + "github.com/pkg/errors.TestFormatWrapf\n" + + "\t.+/github.com/pkg/errors/format_test.go:134", + }, { + Wrapf(New("error"), "error%d", 2), + "%s", + "error2: error", + }, { + Wrapf(New("error"), "error%d", 2), + "%v", + "error2: error", + }, { + Wrapf(New("error"), "error%d", 2), + "%+v", + "error\n" + + "github.com/pkg/errors.TestFormatWrapf\n" + + "\t.+/github.com/pkg/errors/format_test.go:149", + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.error, tt.format, tt.want) + } +} + +func TestFormatWithStack(t *testing.T) { + tests := []struct { + error + format string + want []string + }{{ + WithStack(io.EOF), + "%s", + []string{"EOF"}, + }, { + WithStack(io.EOF), + "%v", + []string{"EOF"}, + }, { + WithStack(io.EOF), + "%+v", + []string{"EOF", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:175"}, + }, { + WithStack(New("error")), + "%s", + []string{"error"}, + }, { + WithStack(New("error")), + "%v", + []string{"error"}, + }, { + WithStack(New("error")), + "%+v", + []string{"error", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:189", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:189"}, + }, { + WithStack(WithStack(io.EOF)), + "%+v", + []string{"EOF", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:197", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:197"}, + }, { + WithStack(WithStack(Wrapf(io.EOF, "message"))), + "%+v", + []string{"EOF", + "message", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:205", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:205", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:205"}, + }, { + WithStack(Errorf("error%d", 1)), + "%+v", + []string{"error1", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:216", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:216"}, + }} + + for i, tt := range tests { + testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) + } +} + +func TestFormatWithMessage(t *testing.T) { + tests := []struct { + error + format string + want []string + }{{ + WithMessage(New("error"), "error2"), + "%s", + []string{"error2: error"}, + }, { + WithMessage(New("error"), "error2"), + "%v", + []string{"error2: error"}, + }, { + WithMessage(New("error"), "error2"), + "%+v", + []string{ + "error", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:244", + "error2"}, + }, { + WithMessage(io.EOF, "addition1"), + "%s", + []string{"addition1: EOF"}, + }, { + WithMessage(io.EOF, "addition1"), + "%v", + []string{"addition1: EOF"}, + }, { + WithMessage(io.EOF, "addition1"), + "%+v", + []string{"EOF", "addition1"}, + }, { + WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), + "%v", + []string{"addition2: addition1: EOF"}, + }, { + WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), + "%+v", + []string{"EOF", "addition1", "addition2"}, + }, { + Wrap(WithMessage(io.EOF, "error1"), "error2"), + "%+v", + []string{"EOF", "error1", "error2", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:272"}, + }, { + WithMessage(Errorf("error%d", 1), "error2"), + "%+v", + []string{"error1", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:278", + "error2"}, + }, { + WithMessage(WithStack(io.EOF), "error"), + "%+v", + []string{ + "EOF", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:285", + "error"}, + }, { + WithMessage(Wrap(WithStack(io.EOF), "inside-error"), "outside-error"), + "%+v", + []string{ + "EOF", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:293", + "inside-error", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:293", + "outside-error"}, + }} + + for i, tt := range tests { + testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) + } +} + +func TestFormatGeneric(t *testing.T) { + starts := []struct { + err error + want []string + }{ + {New("new-error"), []string{ + "new-error", + "github.com/pkg/errors.TestFormatGeneric\n" + + "\t.+/github.com/pkg/errors/format_test.go:315"}, + }, {Errorf("errorf-error"), []string{ + "errorf-error", + "github.com/pkg/errors.TestFormatGeneric\n" + + "\t.+/github.com/pkg/errors/format_test.go:319"}, + }, {errors.New("errors-new-error"), []string{ + "errors-new-error"}, + }, + } + + wrappers := []wrapper{ + { + func(err error) error { return WithMessage(err, "with-message") }, + []string{"with-message"}, + }, { + func(err error) error { return WithStack(err) }, + []string{ + "github.com/pkg/errors.(func·002|TestFormatGeneric.func2)\n\t" + + ".+/github.com/pkg/errors/format_test.go:333", + }, + }, { + func(err error) error { return Wrap(err, "wrap-error") }, + []string{ + "wrap-error", + "github.com/pkg/errors.(func·003|TestFormatGeneric.func3)\n\t" + + ".+/github.com/pkg/errors/format_test.go:339", + }, + }, { + func(err error) error { return Wrapf(err, "wrapf-error%d", 1) }, + []string{ + "wrapf-error1", + "github.com/pkg/errors.(func·004|TestFormatGeneric.func4)\n\t" + + ".+/github.com/pkg/errors/format_test.go:346", + }, + }, + } + + for s := range starts { + err := starts[s].err + want := starts[s].want + testFormatCompleteCompare(t, s, err, "%+v", want, false) + testGenericRecursive(t, err, want, wrappers, 3) + } +} + +func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) { + got := fmt.Sprintf(format, arg) + gotLines := strings.SplitN(got, "\n", -1) + wantLines := strings.SplitN(want, "\n", -1) + + if len(wantLines) > len(gotLines) { + t.Errorf("test %d: wantLines(%d) > gotLines(%d):\n got: %q\nwant: %q", n+1, len(wantLines), len(gotLines), got, want) + return + } + + for i, w := range wantLines { + match, err := regexp.MatchString(w, gotLines[i]) + if err != nil { + t.Fatal(err) + } + if !match { + t.Errorf("test %d: line %d: fmt.Sprintf(%q, err):\n got: %q\nwant: %q", n+1, i+1, format, got, want) + } + } +} + +var stackLineR = regexp.MustCompile(`\.`) + +// parseBlocks parses input into a slice, where: +// - incase entry contains a newline, its a stacktrace +// - incase entry contains no newline, its a solo line. +// +// Detecting stack boundaries only works incase the WithStack-calls are +// to be found on the same line, thats why it is optionally here. +// +// Example use: +// +// for _, e := range blocks { +// if strings.ContainsAny(e, "\n") { +// // Match as stack +// } else { +// // Match as line +// } +// } +// +func parseBlocks(input string, detectStackboundaries bool) ([]string, error) { + var blocks []string + + stack := "" + wasStack := false + lines := map[string]bool{} // already found lines + + for _, l := range strings.Split(input, "\n") { + isStackLine := stackLineR.MatchString(l) + + switch { + case !isStackLine && wasStack: + blocks = append(blocks, stack, l) + stack = "" + lines = map[string]bool{} + case isStackLine: + if wasStack { + // Detecting two stacks after another, possible cause lines match in + // our tests due to WithStack(WithStack(io.EOF)) on same line. + if detectStackboundaries { + if lines[l] { + if len(stack) == 0 { + return nil, errors.New("len of block must not be zero here") + } + + blocks = append(blocks, stack) + stack = l + lines = map[string]bool{l: true} + continue + } + } + + stack = stack + "\n" + l + } else { + stack = l + } + lines[l] = true + case !isStackLine && !wasStack: + blocks = append(blocks, l) + default: + return nil, errors.New("must not happen") + } + + wasStack = isStackLine + } + + // Use up stack + if stack != "" { + blocks = append(blocks, stack) + } + return blocks, nil +} + +func testFormatCompleteCompare(t *testing.T, n int, arg interface{}, format string, want []string, detectStackBoundaries bool) { + gotStr := fmt.Sprintf(format, arg) + + got, err := parseBlocks(gotStr, detectStackBoundaries) + if err != nil { + t.Fatal(err) + } + + if len(got) != len(want) { + t.Fatalf("test %d: fmt.Sprintf(%s, err) -> wrong number of blocks: got(%d) want(%d)\n got: %s\nwant: %s\ngotStr: %q", + n+1, format, len(got), len(want), prettyBlocks(got), prettyBlocks(want), gotStr) + } + + for i := range got { + if strings.ContainsAny(want[i], "\n") { + // Match as stack + match, err := regexp.MatchString(want[i], got[i]) + if err != nil { + t.Fatal(err) + } + if !match { + t.Fatalf("test %d: block %d: fmt.Sprintf(%q, err):\ngot:\n%q\nwant:\n%q\nall-got:\n%s\nall-want:\n%s\n", + n+1, i+1, format, got[i], want[i], prettyBlocks(got), prettyBlocks(want)) + } + } else { + // Match as message + if got[i] != want[i] { + t.Fatalf("test %d: fmt.Sprintf(%s, err) at block %d got != want:\n got: %q\nwant: %q", n+1, format, i+1, got[i], want[i]) + } + } + } +} + +type wrapper struct { + wrap func(err error) error + want []string +} + +func prettyBlocks(blocks []string, prefix ...string) string { + var out []string + + for _, b := range blocks { + out = append(out, fmt.Sprintf("%v", b)) + } + + return " " + strings.Join(out, "\n ") +} + +func testGenericRecursive(t *testing.T, beforeErr error, beforeWant []string, list []wrapper, maxDepth int) { + if len(beforeWant) == 0 { + panic("beforeWant must not be empty") + } + for _, w := range list { + if len(w.want) == 0 { + panic("want must not be empty") + } + + err := w.wrap(beforeErr) + + // Copy required cause append(beforeWant, ..) modified beforeWant subtly. + beforeCopy := make([]string, len(beforeWant)) + copy(beforeCopy, beforeWant) + + beforeWant := beforeCopy + last := len(beforeWant) - 1 + var want []string + + // Merge two stacks behind each other. + if strings.ContainsAny(beforeWant[last], "\n") && strings.ContainsAny(w.want[0], "\n") { + want = append(beforeWant[:last], append([]string{beforeWant[last] + "((?s).*)" + w.want[0]}, w.want[1:]...)...) + } else { + want = append(beforeWant, w.want...) + } + + testFormatCompleteCompare(t, maxDepth, err, "%+v", want, false) + if maxDepth > 0 { + testGenericRecursive(t, err, want, list, maxDepth-1) + } + } +} diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go new file mode 100644 index 000000000..6b1f2891a --- /dev/null +++ b/vendor/github.com/pkg/errors/stack.go @@ -0,0 +1,178 @@ +package errors + +import ( + "fmt" + "io" + "path" + "runtime" + "strings" +) + +// Frame represents a program counter inside a stack frame. +type Frame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f Frame) pc() uintptr { return uintptr(f) - 1 } + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f Frame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + file, _ := fn.FileLine(f.pc()) + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f Frame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + _, line := fn.FileLine(f.pc()) + return line +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s path of source file relative to the compile time GOPATH +// %+v equivalent to %+s:%d +func (f Frame) Format(s fmt.State, verb rune) { + switch verb { + case 's': + switch { + case s.Flag('+'): + pc := f.pc() + fn := runtime.FuncForPC(pc) + if fn == nil { + io.WriteString(s, "unknown") + } else { + file, _ := fn.FileLine(pc) + fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file) + } + default: + io.WriteString(s, path.Base(f.file())) + } + case 'd': + fmt.Fprintf(s, "%d", f.line()) + case 'n': + name := runtime.FuncForPC(f.pc()).Name() + io.WriteString(s, funcname(name)) + case 'v': + f.Format(s, 's') + io.WriteString(s, ":") + f.Format(s, 'd') + } +} + +// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +type StackTrace []Frame + +func (st StackTrace) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + for _, f := range st { + fmt.Fprintf(s, "\n%+v", f) + } + case s.Flag('#'): + fmt.Fprintf(s, "%#v", []Frame(st)) + default: + fmt.Fprintf(s, "%v", []Frame(st)) + } + case 's': + fmt.Fprintf(s, "%s", []Frame(st)) + } +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case st.Flag('+'): + for _, pc := range *s { + f := Frame(pc) + fmt.Fprintf(st, "\n%+v", f) + } + } + } +} + +func (s *stack) StackTrace() StackTrace { + f := make([]Frame, len(*s)) + for i := 0; i < len(f); i++ { + f[i] = Frame((*s)[i]) + } + return f +} + +func callers() *stack { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// funcname removes the path prefix component of a function's name reported by func.Name(). +func funcname(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + return name[i+1:] +} + +func trimGOPATH(name, file string) string { + // Here we want to get the source file path relative to the compile time + // GOPATH. As of Go 1.6.x there is no direct way to know the compiled + // GOPATH at runtime, but we can infer the number of path segments in the + // GOPATH. We note that fn.Name() returns the function name qualified by + // the import path, which does not include the GOPATH. Thus we can trim + // segments from the beginning of the file path until the number of path + // separators remaining is one more than the number of path separators in + // the function name. For example, given: + // + // GOPATH /home/user + // file /home/user/src/pkg/sub/file.go + // fn.Name() pkg/sub.Type.Method + // + // We want to produce: + // + // pkg/sub/file.go + // + // From this we can easily see that fn.Name() has one less path separator + // than our desired output. We count separators from the end of the file + // path until it finds two more than in the function name and then move + // one character forward to preserve the initial path segment without a + // leading separator. + const sep = "/" + goal := strings.Count(name, sep) + 2 + i := len(file) + for n := 0; n < goal; n++ { + i = strings.LastIndex(file[:i], sep) + if i == -1 { + // not enough separators found, set i so that the slice expression + // below leaves file unmodified + i = -len(sep) + break + } + } + // get back to 0 or trim the leading separator + file = file[i+len(sep):] + return file +} diff --git a/vendor/github.com/pkg/errors/stack_test.go b/vendor/github.com/pkg/errors/stack_test.go new file mode 100644 index 000000000..510c27a9f --- /dev/null +++ b/vendor/github.com/pkg/errors/stack_test.go @@ -0,0 +1,292 @@ +package errors + +import ( + "fmt" + "runtime" + "testing" +) + +var initpc, _, _, _ = runtime.Caller(0) + +func TestFrameLine(t *testing.T) { + var tests = []struct { + Frame + want int + }{{ + Frame(initpc), + 9, + }, { + func() Frame { + var pc, _, _, _ = runtime.Caller(0) + return Frame(pc) + }(), + 20, + }, { + func() Frame { + var pc, _, _, _ = runtime.Caller(1) + return Frame(pc) + }(), + 28, + }, { + Frame(0), // invalid PC + 0, + }} + + for _, tt := range tests { + got := tt.Frame.line() + want := tt.want + if want != got { + t.Errorf("Frame(%v): want: %v, got: %v", uintptr(tt.Frame), want, got) + } + } +} + +type X struct{} + +func (x X) val() Frame { + var pc, _, _, _ = runtime.Caller(0) + return Frame(pc) +} + +func (x *X) ptr() Frame { + var pc, _, _, _ = runtime.Caller(0) + return Frame(pc) +} + +func TestFrameFormat(t *testing.T) { + var tests = []struct { + Frame + format string + want string + }{{ + Frame(initpc), + "%s", + "stack_test.go", + }, { + Frame(initpc), + "%+s", + "github.com/pkg/errors.init\n" + + "\t.+/github.com/pkg/errors/stack_test.go", + }, { + Frame(0), + "%s", + "unknown", + }, { + Frame(0), + "%+s", + "unknown", + }, { + Frame(initpc), + "%d", + "9", + }, { + Frame(0), + "%d", + "0", + }, { + Frame(initpc), + "%n", + "init", + }, { + func() Frame { + var x X + return x.ptr() + }(), + "%n", + `\(\*X\).ptr`, + }, { + func() Frame { + var x X + return x.val() + }(), + "%n", + "X.val", + }, { + Frame(0), + "%n", + "", + }, { + Frame(initpc), + "%v", + "stack_test.go:9", + }, { + Frame(initpc), + "%+v", + "github.com/pkg/errors.init\n" + + "\t.+/github.com/pkg/errors/stack_test.go:9", + }, { + Frame(0), + "%v", + "unknown:0", + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.Frame, tt.format, tt.want) + } +} + +func TestFuncname(t *testing.T) { + tests := []struct { + name, want string + }{ + {"", ""}, + {"runtime.main", "main"}, + {"github.com/pkg/errors.funcname", "funcname"}, + {"funcname", "funcname"}, + {"io.copyBuffer", "copyBuffer"}, + {"main.(*R).Write", "(*R).Write"}, + } + + for _, tt := range tests { + got := funcname(tt.name) + want := tt.want + if got != want { + t.Errorf("funcname(%q): want: %q, got %q", tt.name, want, got) + } + } +} + +func TestTrimGOPATH(t *testing.T) { + var tests = []struct { + Frame + want string + }{{ + Frame(initpc), + "github.com/pkg/errors/stack_test.go", + }} + + for i, tt := range tests { + pc := tt.Frame.pc() + fn := runtime.FuncForPC(pc) + file, _ := fn.FileLine(pc) + got := trimGOPATH(fn.Name(), file) + testFormatRegexp(t, i, got, "%s", tt.want) + } +} + +func TestStackTrace(t *testing.T) { + tests := []struct { + err error + want []string + }{{ + New("ooh"), []string{ + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:172", + }, + }, { + Wrap(New("ooh"), "ahh"), []string{ + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:177", // this is the stack of Wrap, not New + }, + }, { + Cause(Wrap(New("ooh"), "ahh")), []string{ + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:182", // this is the stack of New + }, + }, { + func() error { return New("ooh") }(), []string{ + `github.com/pkg/errors.(func·009|TestStackTrace.func1)` + + "\n\t.+/github.com/pkg/errors/stack_test.go:187", // this is the stack of New + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:187", // this is the stack of New's caller + }, + }, { + Cause(func() error { + return func() error { + return Errorf("hello %s", fmt.Sprintf("world")) + }() + }()), []string{ + `github.com/pkg/errors.(func·010|TestStackTrace.func2.1)` + + "\n\t.+/github.com/pkg/errors/stack_test.go:196", // this is the stack of Errorf + `github.com/pkg/errors.(func·011|TestStackTrace.func2)` + + "\n\t.+/github.com/pkg/errors/stack_test.go:197", // this is the stack of Errorf's caller + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:198", // this is the stack of Errorf's caller's caller + }, + }} + for i, tt := range tests { + x, ok := tt.err.(interface { + StackTrace() StackTrace + }) + if !ok { + t.Errorf("expected %#v to implement StackTrace() StackTrace", tt.err) + continue + } + st := x.StackTrace() + for j, want := range tt.want { + testFormatRegexp(t, i, st[j], "%+v", want) + } + } +} + +func stackTrace() StackTrace { + const depth = 8 + var pcs [depth]uintptr + n := runtime.Callers(1, pcs[:]) + var st stack = pcs[0:n] + return st.StackTrace() +} + +func TestStackTraceFormat(t *testing.T) { + tests := []struct { + StackTrace + format string + want string + }{{ + nil, + "%s", + `\[\]`, + }, { + nil, + "%v", + `\[\]`, + }, { + nil, + "%+v", + "", + }, { + nil, + "%#v", + `\[\]errors.Frame\(nil\)`, + }, { + make(StackTrace, 0), + "%s", + `\[\]`, + }, { + make(StackTrace, 0), + "%v", + `\[\]`, + }, { + make(StackTrace, 0), + "%+v", + "", + }, { + make(StackTrace, 0), + "%#v", + `\[\]errors.Frame{}`, + }, { + stackTrace()[:2], + "%s", + `\[stack_test.go stack_test.go\]`, + }, { + stackTrace()[:2], + "%v", + `\[stack_test.go:225 stack_test.go:272\]`, + }, { + stackTrace()[:2], + "%+v", + "\n" + + "github.com/pkg/errors.stackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:225\n" + + "github.com/pkg/errors.TestStackTraceFormat\n" + + "\t.+/github.com/pkg/errors/stack_test.go:276", + }, { + stackTrace()[:2], + "%#v", + `\[\]errors.Frame{stack_test.go:225, stack_test.go:284}`, + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.StackTrace, tt.format, tt.want) + } +}