From 196b4062fe1d01a8feff24b8bb0f482253c520b0 Mon Sep 17 00:00:00 2001 From: David Dymko Date: Thu, 9 Apr 2020 19:40:04 -0400 Subject: [PATCH 1/7] vultr provider --- go.mod | 1 + go.sum | 8 ++ provider/vultr.go | 266 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 275 insertions(+) create mode 100644 provider/vultr.go diff --git a/go.mod b/go.mod index 92fa5e6dc..3060f7140 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( github.com/stretchr/testify v1.4.0 github.com/transip/gotransip v5.8.2+incompatible github.com/vinyldns/go-vinyldns v0.0.0-20190611170422-7119fe55ed92 + github.com/vultr/govultr v0.3.2 go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 diff --git a/go.sum b/go.sum index 4ce44d309..c13e02745 100644 --- a/go.sum +++ b/go.sum @@ -311,8 +311,12 @@ github.com/hashicorp/consul v1.3.0/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= github.com/hashicorp/go-hclog v0.9.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-memdb v1.0.1/go.mod h1:I6dKdmYhZqU0RJSheVEWgTNWdVQH5QvTgIUQ0t/t32M= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -321,6 +325,8 @@ github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uP github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-plugin v1.0.0/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.6.4 h1:BbgctKO892xEyOXnGiaAwIoSq1QZ/SS4AhjoAh9DnfY= +github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -575,6 +581,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ= github.com/vinyldns/go-vinyldns v0.0.0-20190611170422-7119fe55ed92 h1:Q76MzqJu++vAfhj0mVf7t0F4xHUbg+V/d/Uk5PBQjRU= github.com/vinyldns/go-vinyldns v0.0.0-20190611170422-7119fe55ed92/go.mod h1:AZuEfReFWdvtU0LatbLpo70t3lqdLvph2D5mqFP0bkA= +github.com/vultr/govultr v0.3.2 h1:1tV/88jkm+4Y345qAXBe3peNbnmvCY/VAIZApklbKkI= +github.com/vultr/govultr v0.3.2/go.mod h1:81RwK1wAmb08alkFDJiZmu9gdv+IO+UamzaF0+PIieE= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= diff --git a/provider/vultr.go b/provider/vultr.go new file mode 100644 index 000000000..b13f2d200 --- /dev/null +++ b/provider/vultr.go @@ -0,0 +1,266 @@ +/* +Copyright 2020 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 ( + "context" + "fmt" + "os" + "strconv" + + log "github.com/sirupsen/logrus" + "github.com/vultr/govultr" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" +) + +const ( + vultrCreate = "CREATE" + vultrDelete = "DELETE" + vultrUpdate = "UPDATE" + vultrTTL = 3600 +) + +type VultrProvider struct { + client govultr.Client + + domainFilter endpoint.DomainFilter + DryRun bool +} + +type VultrChanges struct { + Action string + + ResourceRecordSet govultr.DNSRecord +} + +// NewVultrProvider initializes a new Vultr BNS based provider +func NewVultrProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*VultrProvider, error) { + apiKey, ok := os.LookupEnv("VULTR_API_KEY") + if !ok { + return nil, fmt.Errorf("no token found") + } + + client := govultr.NewClient(nil, apiKey) + client.SetUserAgent(fmt.Sprintf("ExternalDNS/%s", client.UserAgent)) + + provider := &VultrProvider{ + client: *client, + domainFilter: domainFilter, + DryRun: dryRun, + } + + return provider, nil +} + +// Zones returns list of hosted zones +func (p *VultrProvider) Zones(ctx context.Context) ([]govultr.DNSDomain, error) { + zones, err := p.fetchZones(ctx) + if err != nil { + return nil, err + } + + return zones, nil +} + +func (p *VultrProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { + zones, err := p.Zones(ctx) + if err != nil { + return nil, err + } + + var endpoints []*endpoint.Endpoint + + for _, zone := range zones { + records, err := p.fetchRecords(ctx, zone.Domain) + if err != nil { + return nil, err + } + + for _, r := range records { + if supportedRecordType(r.Type) { + name := fmt.Sprintf("%s.%s", r.Name, zone.Domain) + + // root name is identified by the empty string and should be + // translated to zone name for the endpoint entry. + if r.Name == "" { + name = zone.Domain + } + + endPointTTL := endpoint.NewEndpointWithTTL(name, r.Type, endpoint.TTL(r.TTL), r.Data) + endpoints = append(endpoints, endPointTTL) + } + } + } + return endpoints, nil +} + +func (p *VultrProvider) fetchRecords(ctx context.Context, domain string) ([]govultr.DNSRecord, error) { + records, err := p.client.DNSRecord.List(ctx, domain) + if err != nil { + return nil, err + } + + return records, nil +} + +func (p *VultrProvider) fetchZones(ctx context.Context) ([]govultr.DNSDomain, error) { + var zones []govultr.DNSDomain + + allZones, err := p.client.DNSDomain.List(ctx) + if err != nil { + return nil, err + } + + for _, zone := range allZones { + if p.domainFilter.Match(zone.Domain) { + zones = append(zones, zone) + } + } + + return zones, nil +} + +func (p *VultrProvider) submitChanges(ctx context.Context, changes []*VultrChanges) error { + if len(changes) == 0 { + log.Infof("All records are already up to date") + return nil + } + + zones, err := p.Zones(ctx) + if err != nil { + return err + } + + zoneChanges := seperateChangesByZone(zones, changes) + + for zoneName, changes := range zoneChanges { + for _, change := range changes { + + switch change.Action { + case vultrCreate: + err = p.client.DNSRecord.Create(ctx, zoneName, change.ResourceRecordSet.Type, change.ResourceRecordSet.Name, change.ResourceRecordSet.Data, change.ResourceRecordSet.TTL, change.ResourceRecordSet.Priority) + if err != nil { + return err + } + case vultrDelete: + id, err := p.getRecordID(ctx, zoneName, change.ResourceRecordSet) + if err != nil { + return err + } + + err = p.client.DNSRecord.Delete(ctx, zoneName, strconv.Itoa(id)) + if err != nil { + return err + } + case vultrUpdate: + id, err := p.getRecordID(ctx, zoneName, change.ResourceRecordSet) + if err != nil { + return err + } + + record := &govultr.DNSRecord{ + RecordID: id, + Type: change.ResourceRecordSet.Type, + Name: change.ResourceRecordSet.Name, + Data: change.ResourceRecordSet.Data, + TTL: change.ResourceRecordSet.TTL, + } + + err = p.client.DNSRecord.Update(ctx, zoneName, record) + if err != nil { + return err + } + } + } + } + + return nil +} + +func (p *VultrProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + combinedChanges := make([]*VultrChanges, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) + + combinedChanges = append(combinedChanges, newVultrChanges(vultrCreate, changes.Create)...) + combinedChanges = append(combinedChanges, newVultrChanges(vultrUpdate, changes.UpdateNew)...) + combinedChanges = append(combinedChanges, newVultrChanges(vultrDelete, changes.Delete)...) + + return p.submitChanges(ctx, combinedChanges) +} + +func newVultrChange(action string, endpoint *endpoint.Endpoint) *VultrChanges { + ttl := vultrTTL + + if endpoint.RecordTTL.IsConfigured() { + ttl = int(endpoint.RecordTTL) + } + + change := &VultrChanges{ + Action: action, + ResourceRecordSet: govultr.DNSRecord{ + Type: endpoint.RecordType, + Name: endpoint.DNSName, + Data: endpoint.Targets[0], + TTL: ttl, + }, + } + return change +} + +func newVultrChanges(action string, endpoints []*endpoint.Endpoint) []*VultrChanges { + changes := make([]*VultrChanges, 0, len(endpoints)) + for _, e := range endpoints { + changes = append(changes, newVultrChange(action, e)) + } + return changes +} + +func seperateChangesByZone(zones []govultr.DNSDomain, changes []*VultrChanges) map[string][]*VultrChanges { + change := make(map[string][]*VultrChanges) + zoneNameID := zoneIDName{} + + for _, z := range zones { + zoneNameID.Add(z.Domain, z.Domain) + change[z.Domain] = []*VultrChanges{} + } + + for _, c := range changes { + zone, _ := zoneNameID.FindZone(c.ResourceRecordSet.Name) + if zone == "" { + log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.ResourceRecordSet.Name) + continue + } + change[zone] = append(change[zone], c) + + } + return change +} + +func (p *VultrProvider) getRecordID(ctx context.Context, zone string, record govultr.DNSRecord) (recordID int, err error) { + records, err := p.client.DNSRecord.List(ctx, zone) + if err != nil { + return 0, err + } + + for _, r := range records { + if r.Name == record.Name && r.Type == record.Type { + return recordID, nil + } + } + + return 0, fmt.Errorf("no record was found") +} From a80d7cf6fca4e93879a5b4a50d6b7ed52ebc840e Mon Sep 17 00:00:00 2001 From: David Dymko Date: Fri, 10 Apr 2020 08:09:59 -0400 Subject: [PATCH 2/7] adding vultr provider spots --- .github/labeler.yml | 3 +++ main.go | 2 ++ pkg/apis/externaldns/types.go | 2 +- provider/vultr_test.go | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 provider/vultr_test.go diff --git a/.github/labeler.yml b/.github/labeler.yml index 2ed29809e..0274c9d9d 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -61,3 +61,6 @@ provider/oci: provider/oci* # Add 'provider/vinyldns' in file which starts with vinyldns provider/vinyldns: provider/vinyldns* + +# Add 'provider/vultr' in file which starts with vultr +provider/vultr: provider/vultr* diff --git a/main.go b/main.go index b31b0884c..a09e21d47 100644 --- a/main.go +++ b/main.go @@ -165,6 +165,8 @@ func main() { p, err = provider.NewAzurePrivateDNSProvider(domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureSubscriptionID, cfg.DryRun) case "vinyldns": p, err = provider.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun) + case "vultr": + p, err = provider.NewVultrProvider(domainFilter, cfg.DryRun) case "cloudflare": p, err = provider.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareZonesPerPage, cfg.CloudflareProxied, cfg.DryRun) case "rcodezero": diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index c567a68ea..5bfdeaaf3 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -312,7 +312,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter) // Flags related to providers - app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns") + app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, vultr)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "vultr") app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains) app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter) diff --git a/provider/vultr_test.go b/provider/vultr_test.go new file mode 100644 index 000000000..4f504f668 --- /dev/null +++ b/provider/vultr_test.go @@ -0,0 +1 @@ +package provider From b645fe82bdab11f292da8ce79d9cebeb51a43fb0 Mon Sep 17 00:00:00 2001 From: David Dymko Date: Fri, 10 Apr 2020 11:15:06 -0400 Subject: [PATCH 3/7] vultr provider updates --- provider/vultr.go | 56 ++++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/provider/vultr.go b/provider/vultr.go index b13f2d200..869ace7c3 100644 --- a/provider/vultr.go +++ b/provider/vultr.go @@ -21,6 +21,7 @@ import ( "fmt" "os" "strconv" + "strings" log "github.com/sirupsen/logrus" "github.com/vultr/govultr" @@ -49,7 +50,7 @@ type VultrChanges struct { } // NewVultrProvider initializes a new Vultr BNS based provider -func NewVultrProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*VultrProvider, error) { +func NewVultrProvider(domainFilter endpoint.DomainFilter, dryRun bool) (*VultrProvider, error) { apiKey, ok := os.LookupEnv("VULTR_API_KEY") if !ok { return nil, fmt.Errorf("no token found") @@ -151,6 +152,14 @@ func (p *VultrProvider) submitChanges(ctx context.Context, changes []*VultrChang for zoneName, changes := range zoneChanges { for _, change := range changes { + log.WithFields(log.Fields{ + "record": change.ResourceRecordSet.Name, + "type": change.ResourceRecordSet.Type, + "ttl": change.ResourceRecordSet.TTL, + "action": change.Action, + "zone": zoneName, + }).Info("Changing record.") + switch change.Action { case vultrCreate: err = p.client.DNSRecord.Create(ctx, zoneName, change.ResourceRecordSet.Type, change.ResourceRecordSet.Name, change.ResourceRecordSet.Data, change.ResourceRecordSet.TTL, change.ResourceRecordSet.Priority) @@ -202,29 +211,25 @@ func (p *VultrProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) return p.submitChanges(ctx, combinedChanges) } -func newVultrChange(action string, endpoint *endpoint.Endpoint) *VultrChanges { - ttl := vultrTTL - - if endpoint.RecordTTL.IsConfigured() { - ttl = int(endpoint.RecordTTL) - } - - change := &VultrChanges{ - Action: action, - ResourceRecordSet: govultr.DNSRecord{ - Type: endpoint.RecordType, - Name: endpoint.DNSName, - Data: endpoint.Targets[0], - TTL: ttl, - }, - } - return change -} - func newVultrChanges(action string, endpoints []*endpoint.Endpoint) []*VultrChanges { changes := make([]*VultrChanges, 0, len(endpoints)) + ttl := vultrTTL for _, e := range endpoints { - changes = append(changes, newVultrChange(action, e)) + + if e.RecordTTL.IsConfigured() { + ttl = int(e.RecordTTL) + } + + change := &VultrChanges{ + Action: action, + ResourceRecordSet: govultr.DNSRecord{ + Type: e.RecordType, + Name: e.DNSName, + Data: e.Targets[0], + TTL: ttl, + }, + } + changes = append(changes, change) } return changes } @@ -257,8 +262,13 @@ func (p *VultrProvider) getRecordID(ctx context.Context, zone string, record gov } for _, r := range records { - if r.Name == record.Name && r.Type == record.Type { - return recordID, nil + strippedName := strings.TrimSuffix(record.Name, "."+zone) + if record.Name == zone { + strippedName = "" + } + + if r.Name == strippedName && r.Type == record.Type { + return r.RecordID, nil } } From 1992b693f6b7ff3109e362dd135ea1b93e592278 Mon Sep 17 00:00:00 2001 From: David Dymko Date: Fri, 10 Apr 2020 12:27:30 -0400 Subject: [PATCH 4/7] vultr test --- provider/vultr_test.go | 190 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/provider/vultr_test.go b/provider/vultr_test.go index 4f504f668..592321df0 100644 --- a/provider/vultr_test.go +++ b/provider/vultr_test.go @@ -1 +1,191 @@ +/* +Copyright 2020 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 ( + "context" + "os" + "reflect" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vultr/govultr" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" +) + +type mockVultrDomain struct { + client *govultr.Client +} + +func (m *mockVultrDomain) Create(ctx context.Context, domain, InstanceIP string) error { + return nil +} + +func (m *mockVultrDomain) Delete(ctx context.Context, domain string) error { + return nil +} + +func (m *mockVultrDomain) ToggleDNSSec(ctx context.Context, domain string, enabled bool) error { + return nil +} + +func (m *mockVultrDomain) DNSSecInfo(ctx context.Context, domain string) ([]string, error) { + return nil, nil +} + +func (m *mockVultrDomain) List(ctx context.Context) ([]govultr.DNSDomain, error) { + return []govultr.DNSDomain{{Domain: "test.com"}}, nil +} + +func (m *mockVultrDomain) GetSoa(ctx context.Context, domain string) (*govultr.Soa, error) { + return nil, nil +} + +func (m *mockVultrDomain) UpdateSoa(ctx context.Context, domain, nsPrimary, email string) error { + return nil +} + +type mockVultrRecord struct { + client *govultr.Client +} + +func (m *mockVultrRecord) Create(ctx context.Context, domain, recordType, name, data string, ttl, priority int) error { + return nil +} + +func (m *mockVultrRecord) Delete(ctx context.Context, domain, recordID string) error { + return nil +} + +func (m *mockVultrRecord) List(ctx context.Context, domain string) ([]govultr.DNSRecord, error) { + return []govultr.DNSRecord{{RecordID: 123, Type: "A", Name: "test", Data: "192.168.1.1", TTL: 300}}, nil +} + +func (m *mockVultrRecord) Update(ctx context.Context, domain string, dnsRecord *govultr.DNSRecord) error { + return nil +} + +func TestNewVultrProvider(t *testing.T) { + _ = os.Setenv("VULTR_API_KEY", "") + _, err := NewVultrProvider(endpoint.NewDomainFilter([]string{"test.vultr.com"}), true) + if err != nil { + t.Errorf("failed : %s", err) + } + + _ = os.Unsetenv("VULTR_API_KEY") + _, err = NewVultrProvider(endpoint.NewDomainFilter([]string{"test.vultr.com"}), true) + if err == nil { + t.Errorf("expected to fail") + } +} + +func TestVultrProvider_Zones(t *testing.T) { + mocked := mockVultrDomain{nil} + provider := &VultrProvider{ + client: *&govultr.Client{ + DNSDomain: &mocked, + }, + } + + expected, err := provider.client.DNSDomain.List(context.Background()) + if err != nil { + t.Fatal(err) + } + zones, err := provider.Zones(context.Background()) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(expected, zones) { + t.Fatal(err) + } +} + +func TestVultrProvider_Records(t *testing.T) { + mocked := mockVultrRecord{nil} + mockedDomain := mockVultrDomain{nil} + + provider := &VultrProvider{ + client: *&govultr.Client{ + DNSRecord: &mocked, + DNSDomain: &mockedDomain, + }, + } + + expected, _ := provider.client.DNSRecord.List(context.Background(), "test.com") + records, err := provider.Records(context.Background()) + if err != nil { + t.Fatal(err) + } + + for _, v := range records { + assert.Equal(t, strings.TrimSuffix(v.DNSName, ".test.com"), expected[0].Name) + assert.Equal(t, v.RecordType, expected[0].Type) + assert.Equal(t, int(v.RecordTTL), expected[0].TTL) + } + +} + +func TestVultrProvider_ApplyChanges(t *testing.T) { + changes := &plan.Changes{} + mocked := mockVultrRecord{nil} + mockedDomain := mockVultrDomain{nil} + + provider := &VultrProvider{ + client: *&govultr.Client{ + DNSRecord: &mocked, + DNSDomain: &mockedDomain, + }, + } + + changes.Create = []*endpoint.Endpoint{ + {DNSName: "test.com", Targets: endpoint.Targets{"target"}}, + {DNSName: "ttl.test.com", Targets: endpoint.Targets{"target"}, RecordTTL: 100}, + } + + changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test.test.com", Targets: endpoint.Targets{"target-new"}, RecordType: "A", RecordTTL: 100}} + changes.Delete = []*endpoint.Endpoint{{DNSName: "test.test.com", Targets: endpoint.Targets{"target"}, RecordType: "A"}} + err := provider.ApplyChanges(context.Background(), changes) + if err != nil { + t.Errorf("should not fail, %s", err) + } +} + +func TestVultrProvider_getRecordID(t *testing.T) { + mocked := mockVultrRecord{nil} + mockedDomain := mockVultrDomain{nil} + + provider := &VultrProvider{ + client: *&govultr.Client{ + DNSRecord: &mocked, + DNSDomain: &mockedDomain, + }, + } + + record := govultr.DNSRecord{ + RecordID: 123, + Type: "A", + Name: "test.test.com", + } + id, err := provider.getRecordID(context.Background(), "test.com", record) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, id, record.RecordID) +} From beedea29775f2ac479e35d1f40e342a8082a3ef6 Mon Sep 17 00:00:00 2001 From: David Dymko Date: Fri, 10 Apr 2020 12:46:59 -0400 Subject: [PATCH 5/7] docs --- docs/ttl.md | 4 + docs/tutorials/vultr.md | 188 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 docs/tutorials/vultr.md diff --git a/docs/ttl.md b/docs/ttl.md index 8855131e1..8e5df4e0f 100644 --- a/docs/ttl.md +++ b/docs/ttl.md @@ -44,6 +44,7 @@ Providers - [x] Linode - [x] TransIP - [x] RFC2136 +- [x] Vultr PRs welcome! @@ -72,3 +73,6 @@ The Linode Provider default TTL is used when the TTL is 0. The default is 24 hou ### TransIP Provider The TransIP Provider minimal TTL is used when the TTL is 0. The minimal TTL is 60s. + +### Vultr Provider +The Vultr provider minimal TTL is used when the TTL is 0. The default is 1 hour. diff --git a/docs/tutorials/vultr.md b/docs/tutorials/vultr.md new file mode 100644 index 000000000..7bd0a9f5b --- /dev/null +++ b/docs/tutorials/vultr.md @@ -0,0 +1,188 @@ +# Setting up ExternalDNS for Services on Vultr + +This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Vultr DNS. + +Make sure to use **>=0.6** version of ExternalDNS for this tutorial. + +## Managing DNS with Vultr + +If you want to read up on vultr DNS service you can read the following tutorial: +[Introduction to Vultr DNS](https://www.vultr.com/docs/introduction-to-vultr-dns) + +Create a new DNS Zone where you want to create your records in. For the examples we will be using `example.com` + +## Creating Vultr Credentials + +You will need to create a new API Key which can be found on the [Vultr Dashboard](https://my.vultr.com/settings/#settingsapi). + +The environment variable `VULTR_API_KEY` will be needed to run ExternalDNS with Vultr. + +## Deploy ExternalDNS + +Connect your `kubectl` client to the cluster you want to test ExternalDNS with. +Then apply one of the following manifests file to deploy ExternalDNS. + +### Manifest (for clusters without RBAC enabled) + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + containers: + - name: external-dns + image: registry.opensource.zalan.do/teapot/external-dns:latest + args: + - --source=service # ingress is also possible + - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. + - --provider=vultr + env: + - name: VULTR_API_KEY + value: "YOU_VULTR_API_KEY" +``` + +### Manifest (for clusters with RBAC enabled) + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: external-dns +rules: +- apiGroups: [""] + resources: ["services","endpoints","pods"] + verbs: ["get","watch","list"] +- apiGroups: ["extensions"] + resources: ["ingresses"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["nodes"] + verbs: ["list"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: external-dns-viewer +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: external-dns +subjects: +- kind: ServiceAccount + name: external-dns + namespace: default +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + serviceAccountName: external-dns + containers: + - name: external-dns + image: registry.opensource.zalan.do/teapot/external-dns:latest + args: + - --source=service # ingress is also possible + - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. + - --provider=vultr + env: + - name: VULTR_API_KEY + value: "YOU_VULTR_API_KEY" +``` + +## Deploying an Nginx Service + +Create a service file called 'nginx.yaml' with the following contents: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx +spec: + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - image: nginx + name: nginx + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx + annotations: + external-dns.alpha.kubernetes.io/hostname: my-app.example.com +spec: + selector: + app: nginx + type: LoadBalancer + ports: + - protocol: TCP + port: 80 + targetPort: 80 +``` + +Note the annotation on the service; use the same hostname as the Vultr DNS zone created above. + +ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. + +Create the deployment and service: + +```console +$ kubectl create -f nginx.yaml +``` + +Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. + +Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Vultr DNS records. + +## Verifying Vultr DNS records + +Check your [Vultr UI](https://my.vultr.com/dns/) to view the records for your Vultr DNS zone. + +Click on the zone for the one created above if a different domain was used. + +This should show the external IP address of the service as the A record for your domain. + +## Cleanup + +Now that we have verified that ExternalDNS will automatically manage Vultr DNS records, we can delete the tutorial's example: + +``` +$ kubectl delete service -f nginx.yaml +$ kubectl delete service -f externaldns.yaml +``` From 11410cd5278b72eb7cfdfe942d1ef1fab5e869bf Mon Sep 17 00:00:00 2001 From: David Dymko Date: Fri, 10 Apr 2020 13:01:24 -0400 Subject: [PATCH 6/7] readme update --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6e894e4d2..7c6e4337f 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ The following table clarifies the current status of the providers according to t | RancherDNS | Alpha | | | Akamai FastDNS | Alpha | | | OVH | Alpha | | +| Vultr | Alpha | | ## Running ExternalDNS: @@ -142,6 +143,7 @@ The following tutorials are provided: * [TransIP](docs/tutorials/transip.md) * [VinylDNS](docs/tutorials/vinyldns.md) * [OVH](docs/tutorials/ovh.md) +* [Vultr](docs/tutorials/vultr.md) ### Running Locally From b140ca845debc07f9819982a75b0941aa139a800 Mon Sep 17 00:00:00 2001 From: David Dymko Date: Fri, 10 Apr 2020 13:28:39 -0400 Subject: [PATCH 7/7] removing typo --- provider/vultr_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/provider/vultr_test.go b/provider/vultr_test.go index 592321df0..73bd0e02a 100644 --- a/provider/vultr_test.go +++ b/provider/vultr_test.go @@ -98,7 +98,7 @@ func TestNewVultrProvider(t *testing.T) { func TestVultrProvider_Zones(t *testing.T) { mocked := mockVultrDomain{nil} provider := &VultrProvider{ - client: *&govultr.Client{ + client: govultr.Client{ DNSDomain: &mocked, }, } @@ -121,7 +121,7 @@ func TestVultrProvider_Records(t *testing.T) { mockedDomain := mockVultrDomain{nil} provider := &VultrProvider{ - client: *&govultr.Client{ + client: govultr.Client{ DNSRecord: &mocked, DNSDomain: &mockedDomain, }, @@ -147,7 +147,7 @@ func TestVultrProvider_ApplyChanges(t *testing.T) { mockedDomain := mockVultrDomain{nil} provider := &VultrProvider{ - client: *&govultr.Client{ + client: govultr.Client{ DNSRecord: &mocked, DNSDomain: &mockedDomain, }, @@ -171,7 +171,7 @@ func TestVultrProvider_getRecordID(t *testing.T) { mockedDomain := mockVultrDomain{nil} provider := &VultrProvider{ - client: *&govultr.Client{ + client: govultr.Client{ DNSRecord: &mocked, DNSDomain: &mockedDomain, },