diff --git a/Gopkg.lock b/Gopkg.lock index 0d8e691892..6ba5fc6e50 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -190,6 +190,12 @@ ] revision = "09691a3b6378b740595c1002f40c34dd5f218a22" +[[projects]] + name = "github.com/exoscale/egoscale" + packages = ["."] + revision = "631ee6ea16ccb48a0c98054fdbf0f6e94d8f4a8c" + version = "v0.9.31" + [[projects]] branch = "master" name = "github.com/ffledgling/pdns-go" @@ -288,6 +294,12 @@ packages = ["."] revision = "61dc5f9b0a655ebf43026f0d8a837ad1e28e4b96" +[[projects]] + branch = "master" + name = "github.com/jinzhu/copier" + packages = ["."] + revision = "7e38e58719c33e0d44d585c4ab477a30f8cb82dd" + [[projects]] name = "github.com/jmespath/go-jmespath" packages = ["."] @@ -679,6 +691,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "66dc2d3612a3cea92d6533aef837db593aa9b49b2eeffe724d7211ceba87294b" + inputs-digest = "d704eb6432ef9b41338900e647421a195366f87134918f9feb023fc377064f57" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 1e317c2021..6cdb5c119d 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -68,6 +68,10 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"] name = "github.com/nesv/go-dynect" version = "0.6.0" +[[constraint]] + name = "github.com/exoscale/egoscale" + version = "~0.9.31" + [[constraint]] name = "github.com/oracle/oci-go-sdk" version = "1.8.0" diff --git a/README.md b/README.md index 62599c72c8..9a847370be 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ ExternalDNS' current release is `v0.5`. This version allows you to keep selected * [OpenStack Designate](https://docs.openstack.org/designate/latest/) * [PowerDNS](https://www.powerdns.com/) * [CoreDNS](https://coredns.io/) +* [Exoscale](https://www.exoscale.com/dns/) * [Oracle Cloud Infrastructure DNS](https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm) From this release, ExternalDNS can become aware of the records it is managing (enabled via `--registry=txt`), therefore ExternalDNS can safely manage non-empty hosted zones. We strongly encourage you to use `v0.5` (or greater) with `--registry=txt` enabled and `--txt-owner-id` set to a unique value that doesn't change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (`--dry-run` flag) to see the changes to be submitted to your DNS Provider API. @@ -58,6 +59,7 @@ The following tutorials are provided: * Google Container Engine * [Using Google's Default Ingress Controller](docs/tutorials/gke.md) * [Using the Nginx Ingress Controller](docs/tutorials/nginx-ingress.md) +* [Exoscale](docs/tutorials/exoscale.md) * [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md) ## Running Locally diff --git a/docs/tutorials/exoscale.md b/docs/tutorials/exoscale.md new file mode 100644 index 0000000000..c05c7c639e --- /dev/null +++ b/docs/tutorials/exoscale.md @@ -0,0 +1,158 @@ +# Setting up ExternalDNS for Exoscale + +## Prerequisites + +Exoscale provider support was added via [this PR](https://github.com/kubernetes-incubator/external-dns/pull/625), thus you need to use external-dns v0.5.5. + +The Exoscale provider expects that your Exoscale zones, you wish to add records to, already exists +and are configured correctly. It does not add, remove or configure new zones in anyway. + +To do this pease refer to the [Exoscale DNS documentation](https://community.exoscale.com/documentation/dns/). + +Additionally you will have to provide the Exoscale...: + +* API Key +* API Secret +* API Endpoint +* Elastic IP address, to access the workers + +## Deployment + +Deploying external DNS for Exoscale is actually nearly identical to deploying +it for other providers. This is what a sample `deployment.yaml` looks like: + +```yaml +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + template: + metadata: + labels: + app: external-dns + spec: + # Only use if you're also using RBAC + # serviceAccountName: external-dns + containers: + - name: external-dns + image: registry.opensource.zalan.do/teapot/external-dns:v0.5.5 + args: + - --source=ingress # or service or both + - --provider=exoscale + - --domain-filter={{ my-domain }} + - --policy=sync # if you want DNS entries to get deleted as well + - --txt-owner-id={{ owner-id-for-this-external-dns }} + - --exoscale-endpoint={{ endpoint }} # usually https://api.exoscale.ch/dns + - --exoscale-apikey={{ api-key}} + - --exoscale-apisecret={{ api-secret }} +``` + +## RBAC + +If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns: + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns + namespace: default + +--- + +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: external-dns +rules: +- apiGroups: [""] + resources: ["services"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["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 +``` + +## Testing and Verification + +**Important!**: Remember to change `example.com` with your own domain throughout the following text. + +Spin up a simple nginx HTTP server with the following spec (`kubectl apply -f`): + +```yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: nginx + annotations: + kubernetes.io/ingress.class: nginx + external-dns.alpha.kubernetes.io/target: {{ Elastic-IP-address }} +spec: + rules: + - host: via-ingress.example.com + http: + paths: + - backend: + serviceName: nginx + servicePort: 80 + +--- + +apiVersion: v1 +kind: Service +metadata: + name: nginx +spec: + ports: + - port: 80 + targetPort: 80 + selector: + app: nginx + +--- + +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: nginx +spec: + template: + metadata: + labels: + app: nginx + spec: + containers: + - image: nginx + name: nginx + ports: + - containerPort: 80 +``` + +**Important!**: Don't run dig, nslookup or similar immediately (until you've +confirmed the record exists). You'll get hit by [negative DNS caching](https://tools.ietf.org/html/rfc2308), which is hard to flush. + +Wait about 30s-1m (interval for external-dns to kick in), then check Exoscales [portal](https://portal.exoscale.com/dns/example.com)... via-ingress.example.com should appear as a A and TXT record with your Elastic-IP-address. \ No newline at end of file diff --git a/main.go b/main.go index bc6d656d23..76c913b1f2 100644 --- a/main.go +++ b/main.go @@ -151,6 +151,8 @@ func main() { ) case "coredns", "skydns": p, err = provider.NewCoreDNSProvider(domainFilter, cfg.DryRun) + case "exoscale": + p, err = provider.NewExoscaleProvider(cfg.ExoscaleEndpoint, cfg.ExoscaleAPIKey, cfg.ExoscaleAPISecret, cfg.DryRun, provider.ExoscaleWithDomain(domainFilter), provider.ExoscaleWithLogging()), nil case "inmemory": p, err = provider.NewInMemoryProvider(provider.InMemoryInitZones(cfg.InMemoryZones), provider.InMemoryWithDomain(domainFilter), provider.InMemoryWithLogging()), nil case "designate": diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index c43ffc2295..dc8cdd7983 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -86,6 +86,9 @@ type Config struct { MetricsAddress string LogLevel string TXTCacheInterval time.Duration + ExoscaleEndpoint string + ExoscaleAPIKey string + ExoscaleAPISecret string } var defaultConfig = &Config{ @@ -134,6 +137,9 @@ var defaultConfig = &Config{ LogFormat: "text", MetricsAddress: ":7979", LogLevel: logrus.InfoLevel.String(), + ExoscaleEndpoint: "https://api.exoscale.ch/dns", + ExoscaleAPIKey: "", + ExoscaleAPISecret: "", } // NewConfig returns new Config object @@ -187,7 +193,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("connector-source-server", "The server to connect for connector source, valid only when using connector source").Default(defaultConfig.ConnectorSourceServer).StringVar(&cfg.ConnectorSourceServer) // Flags related to providers - app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci") + app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale") app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter) app.Flag("google-project", "When using the Google provider, current project is auto-detected, when running on GCP. Specify other project with this. Must be specified when running outside GCP.").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject) @@ -220,6 +226,10 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("tls-client-cert", "When using TLS communication, the path to the certificate to present as a client (not required for TLS)").Default(defaultConfig.TLSClientCert).StringVar(&cfg.TLSClientCert) app.Flag("tls-client-cert-key", "When using TLS communication, the path to the certificate key to use with the client certificate (not required for TLS)").Default(defaultConfig.TLSClientCertKey).StringVar(&cfg.TLSClientCertKey) + app.Flag("exoscale-endpoint", "Provide the endpoint for the Exoscale provider").Default(defaultConfig.ExoscaleEndpoint).StringVar(&cfg.ExoscaleEndpoint) + app.Flag("exoscale-apikey", "Provide your API Key for the Exoscale provider").Default(defaultConfig.ExoscaleAPIKey).StringVar(&cfg.ExoscaleAPIKey) + app.Flag("exoscale-apisecret", "Provide your API Secret for the Exoscale provider").Default(defaultConfig.ExoscaleAPISecret).StringVar(&cfg.ExoscaleAPISecret) + // Flags related to policies app.Flag("policy", "Modify how DNS records are sychronized between sources and providers (default: sync, options: sync, upsert-only)").Default(defaultConfig.Policy).EnumVar(&cfg.Policy, "sync", "upsert-only") diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index a5dfa2fbcd..1edacff18b 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -68,6 +68,9 @@ var ( MetricsAddress: ":7979", LogLevel: logrus.InfoLevel.String(), ConnectorSourceServer: "localhost:8080", + ExoscaleEndpoint: "https://api.exoscale.ch/dns", + ExoscaleAPIKey: "", + ExoscaleAPISecret: "", } overriddenConfig = &Config{ @@ -114,6 +117,9 @@ var ( MetricsAddress: "127.0.0.1:9099", LogLevel: logrus.DebugLevel.String(), ConnectorSourceServer: "localhost:8081", + ExoscaleEndpoint: "https://api.foo.ch/dns", + ExoscaleAPIKey: "1", + ExoscaleAPISecret: "2", } ) @@ -184,6 +190,9 @@ func TestParseFlags(t *testing.T) { "--metrics-address=127.0.0.1:9099", "--log-level=debug", "--connector-source-server=localhost:8081", + "--exoscale-endpoint=https://api.foo.ch/dns", + "--exoscale-apikey=1", + "--exoscale-apisecret=2", }, envVars: map[string]string{}, expected: overriddenConfig, @@ -235,6 +244,9 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_METRICS_ADDRESS": "127.0.0.1:9099", "EXTERNAL_DNS_LOG_LEVEL": "debug", "EXTERNAL_DNS_CONNECTOR_SOURCE_SERVER": "localhost:8081", + "EXTERNAL_DNS_EXOSCALE_ENDPOINT": "https://api.foo.ch/dns", + "EXTERNAL_DNS_EXOSCALE_APIKEY": "1", + "EXTERNAL_DNS_EXOSCALE_APISECRET": "2", }, expected: overriddenConfig, }, diff --git a/provider/exoscale.go b/provider/exoscale.go new file mode 100644 index 0000000000..f4e7866ca2 --- /dev/null +++ b/provider/exoscale.go @@ -0,0 +1,255 @@ +/* +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 ( + "strings" + + "github.com/exoscale/egoscale" + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/kubernetes-incubator/external-dns/plan" + log "github.com/sirupsen/logrus" +) + +// EgoscaleClientI for replaceable implementation +type EgoscaleClientI interface { + GetRecords(string) ([]egoscale.DNSRecord, error) + GetDomains() ([]egoscale.DNSDomain, error) + CreateRecord(string, egoscale.DNSRecord) (*egoscale.DNSRecord, error) + DeleteRecord(string, int64) error + UpdateRecord(string, egoscale.UpdateDNSRecord) (*egoscale.DNSRecord, error) +} + +// ExoscaleProvider initialized as dns provider with no records +type ExoscaleProvider struct { + domain DomainFilter + client EgoscaleClientI + filter *zoneFilter + OnApplyChanges func(changes *plan.Changes) + dryRun bool +} + +// ExoscaleOption for Provider options +type ExoscaleOption func(*ExoscaleProvider) + +// NewExoscaleProvider returns ExoscaleProvider DNS provider interface implementation +func NewExoscaleProvider(endpoint, apiKey, apiSecret string, dryRun bool, opts ...ExoscaleOption) *ExoscaleProvider { + client := egoscale.NewClient(endpoint, apiKey, apiSecret) + return NewExoscaleProviderWithClient(endpoint, apiKey, apiSecret, client, dryRun, opts...) +} + +// NewExoscaleProviderWithClient returns ExoscaleProvider DNS provider interface implementation (Client provided) +func NewExoscaleProviderWithClient(endpoint, apiKey, apiSecret string, client EgoscaleClientI, dryRun bool, opts ...ExoscaleOption) *ExoscaleProvider { + ep := &ExoscaleProvider{ + filter: &zoneFilter{}, + OnApplyChanges: func(changes *plan.Changes) {}, + domain: NewDomainFilter([]string{""}), + client: client, + dryRun: dryRun, + } + for _, opt := range opts { + opt(ep) + } + return ep +} + +func (ep *ExoscaleProvider) getZones() (map[int64]string, error) { + dom, err := ep.client.GetDomains() + if err != nil { + return nil, err + } + + zones := map[int64]string{} + for _, d := range dom { + zones[d.ID] = d.Name + } + return zones, nil +} + +// ApplyChanges simply modifies DNS via exoscale API +func (ep *ExoscaleProvider) ApplyChanges(changes *plan.Changes) error { + ep.OnApplyChanges(changes) + + if ep.dryRun { + log.Infof("Will NOT delete these records: %+v", changes.Delete) + log.Infof("Will NOT create these records: %+v", changes.Create) + log.Infof("Will NOT update these records: %+v", merge(changes.UpdateOld, changes.UpdateNew)) + return nil + } + + zones, err := ep.getZones() + if err != nil { + return err + } + + for _, epoint := range changes.Create { + if ep.domain.Match(epoint.DNSName) { + if zoneID, name := ep.filter.EndpointZoneID(epoint, zones); zoneID != 0 { + rec := egoscale.DNSRecord{ + Name: name, + RecordType: epoint.RecordType, + TTL: int(epoint.RecordTTL), + Content: epoint.Targets[0], + } + _, err := ep.client.CreateRecord(zones[zoneID], rec) + if err != nil { + return err + } + } + } + } + for _, epoint := range changes.UpdateNew { + if ep.domain.Match(epoint.DNSName) { + if zoneID, name := ep.filter.EndpointZoneID(epoint, zones); zoneID != 0 { + records, err := ep.client.GetRecords(zones[zoneID]) + if err != nil { + return err + } + for _, r := range records { + if r.Name == name { + rec := egoscale.UpdateDNSRecord{ + ID: r.ID, + DomainID: r.DomainID, + Name: name, + RecordType: epoint.RecordType, + TTL: int(epoint.RecordTTL), + Content: epoint.Targets[0], + Prio: r.Prio, + } + if _, err := ep.client.UpdateRecord(zones[zoneID], rec); err != nil { + return err + } + break + } + } + } + } + } + for _, epoint := range changes.UpdateOld { + // Since Exoscale "Patches", we ignore UpdateOld + // We leave this logging here for information + log.Debugf("UPDATE-OLD (ignored) for epoint: %+v", epoint) + } + for _, epoint := range changes.Delete { + if ep.domain.Match(epoint.DNSName) { + if zoneID, name := ep.filter.EndpointZoneID(epoint, zones); zoneID != 0 { + records, err := ep.client.GetRecords(zones[zoneID]) + if err != nil { + return err + } + + for _, r := range records { + if r.Name == name { + if err := ep.client.DeleteRecord(zones[zoneID], r.ID); err != nil { + return err + } + break + } + } + } + } + } + + return nil +} + +// Records returns the list of endpoints +func (ep *ExoscaleProvider) Records() ([]*endpoint.Endpoint, error) { + endpoints := make([]*endpoint.Endpoint, 0) + + dom, err := ep.client.GetDomains() + if err != nil { + return nil, err + } + + for _, d := range dom { + record, err := ep.client.GetRecords(d.Name) + if err != nil { + return nil, err + } + for _, r := range record { + switch r.RecordType { + case "A", "CNAME", "TXT": + break + default: + continue + } + ep := endpoint.NewEndpointWithTTL(r.Name+"."+d.Name, r.RecordType, endpoint.TTL(r.TTL), r.Content) + endpoints = append(endpoints, ep) + } + } + + log.Infof("called Records() with %d items", len(endpoints)) + return endpoints, nil +} + +// ExoscaleWithDomain modifies the domain on which dns zones are filtered +func ExoscaleWithDomain(domainFilter DomainFilter) ExoscaleOption { + return func(p *ExoscaleProvider) { + p.domain = domainFilter + } +} + +// ExoscaleWithLogging injects logging when ApplyChanges is called +func ExoscaleWithLogging() ExoscaleOption { + return func(p *ExoscaleProvider) { + p.OnApplyChanges = func(changes *plan.Changes) { + for _, v := range changes.Create { + log.Infof("CREATE: %v", v) + } + for _, v := range changes.UpdateOld { + log.Infof("UPDATE (old): %v", v) + } + for _, v := range changes.UpdateNew { + log.Infof("UPDATE (new): %v", v) + } + for _, v := range changes.Delete { + log.Infof("DELETE: %v", v) + } + } + } +} + +type zoneFilter struct { + domain string +} + +// Zones filters map[zoneID]zoneName for names having f.domain as suffix +func (f *zoneFilter) Zones(zones map[int64]string) map[int64]string { + result := map[int64]string{} + for zoneID, zoneName := range zones { + if strings.HasSuffix(zoneName, f.domain) { + result[zoneID] = zoneName + } + } + return result +} + +// EndpointZoneID determines zoneID for endpoint from map[zoneID]zoneName by taking longest suffix zoneName match in endpoint DNSName +// returns 0 if no match found +func (f *zoneFilter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[int64]string) (zoneID int64, name string) { + var matchZoneID int64 + var matchZoneName string + for zoneID, zoneName := range zones { + if strings.HasSuffix(endpoint.DNSName, "."+zoneName) && len(zoneName) > len(matchZoneName) { + matchZoneName = zoneName + matchZoneID = zoneID + name = strings.TrimSuffix(endpoint.DNSName, "."+zoneName) + } + } + return matchZoneID, name +} diff --git a/provider/exoscale_test.go b/provider/exoscale_test.go new file mode 100644 index 0000000000..4c0c5bcbd7 --- /dev/null +++ b/provider/exoscale_test.go @@ -0,0 +1,189 @@ +/* +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 ( + "strings" + "testing" + + "github.com/exoscale/egoscale" + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/kubernetes-incubator/external-dns/plan" + "github.com/stretchr/testify/assert" +) + +type createRecordExoscale struct { + name string + rec egoscale.DNSRecord +} + +type deleteRecordExoscale struct { + name string + recordID int64 +} + +type updateRecordExoscale struct { + name string + updateDNSRecord egoscale.UpdateDNSRecord +} + +var createExoscale []createRecordExoscale +var deleteExoscale []deleteRecordExoscale +var updateExoscale []updateRecordExoscale + +type ExoscaleClientStub struct { +} + +func NewExoscaleClientStub() EgoscaleClientI { + ep := &ExoscaleClientStub{} + return ep +} + +func (ep *ExoscaleClientStub) DeleteRecord(name string, recordID int64) error { + deleteExoscale = append(deleteExoscale, deleteRecordExoscale{name: name, recordID: recordID}) + return nil +} +func (ep *ExoscaleClientStub) GetRecords(name string) ([]egoscale.DNSRecord, error) { + init := []egoscale.DNSRecord{ + {ID: 0, Name: "v4.barfoo.com", RecordType: "ALIAS"}, + {ID: 1, Name: "v1.foo.com", RecordType: "TXT"}, + {ID: 2, Name: "v2.bar.com", RecordType: "A"}, + {ID: 3, Name: "v3.bar.com", RecordType: "ALIAS"}, + {ID: 4, Name: "v2.foo.com", RecordType: "CNAME"}, + {ID: 5, Name: "v1.foobar.com", RecordType: "TXT"}, + } + + rec := make([]egoscale.DNSRecord, 0) + for _, r := range init { + if strings.HasSuffix(r.Name, "."+name) { + r.Name = strings.TrimSuffix(r.Name, "."+name) + rec = append(rec, r) + } + } + + return rec, nil +} +func (ep *ExoscaleClientStub) UpdateRecord(name string, rec egoscale.UpdateDNSRecord) (*egoscale.DNSRecord, error) { + updateExoscale = append(updateExoscale, updateRecordExoscale{name: name, updateDNSRecord: rec}) + return nil, nil +} +func (ep *ExoscaleClientStub) CreateRecord(name string, rec egoscale.DNSRecord) (*egoscale.DNSRecord, error) { + createExoscale = append(createExoscale, createRecordExoscale{name: name, rec: rec}) + return nil, nil +} +func (ep *ExoscaleClientStub) GetDomains() ([]egoscale.DNSDomain, error) { + dom := []egoscale.DNSDomain{ + {ID: 1, Name: "foo.com"}, + {ID: 2, Name: "bar.com"}, + } + return dom, nil +} + +func contains(arr []*endpoint.Endpoint, name string) bool { + for _, a := range arr { + if a.DNSName == name { + return true + } + } + return false +} + +func TestExoscaleGetRecords(t *testing.T) { + provider := NewExoscaleProviderWithClient("", "", "", NewExoscaleClientStub(), false) + + if recs, err := provider.Records(); err == nil { + assert.Equal(t, 3, len(recs)) + assert.True(t, contains(recs, "v1.foo.com")) + assert.True(t, contains(recs, "v2.bar.com")) + assert.True(t, contains(recs, "v2.foo.com")) + assert.False(t, contains(recs, "v3.bar.com")) + assert.False(t, contains(recs, "v1.foobar.com")) + } else { + assert.Error(t, err) + } +} + +func TestExoscaleApplyChanges(t *testing.T) { + provider := NewExoscaleProviderWithClient("", "", "", NewExoscaleClientStub(), false) + + plan := &plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "v1.foo.com", + RecordType: "A", + Targets: []string{""}, + }, + { + DNSName: "v1.foobar.com", + RecordType: "TXT", + Targets: []string{""}, + }, + }, + Delete: []*endpoint.Endpoint{ + { + DNSName: "v1.foo.com", + RecordType: "A", + Targets: []string{""}, + }, + { + DNSName: "v1.foobar.com", + RecordType: "TXT", + Targets: []string{""}, + }, + }, + UpdateOld: []*endpoint.Endpoint{ + { + DNSName: "v1.foo.com", + RecordType: "A", + Targets: []string{""}, + }, + { + DNSName: "v1.foobar.com", + RecordType: "TXT", + Targets: []string{""}, + }, + }, + UpdateNew: []*endpoint.Endpoint{ + { + DNSName: "v1.foo.com", + RecordType: "A", + Targets: []string{""}, + }, + { + DNSName: "v1.foobar.com", + RecordType: "TXT", + Targets: []string{""}, + }, + }, + } + createExoscale = make([]createRecordExoscale, 0) + deleteExoscale = make([]deleteRecordExoscale, 0) + + provider.ApplyChanges(plan) + + assert.Equal(t, 1, len(createExoscale)) + assert.Equal(t, "foo.com", createExoscale[0].name) + assert.Equal(t, "v1", createExoscale[0].rec.Name) + + assert.Equal(t, 1, len(deleteExoscale)) + assert.Equal(t, "foo.com", deleteExoscale[0].name) + assert.Equal(t, int64(1), deleteExoscale[0].recordID) + + assert.Equal(t, 1, len(updateExoscale)) + assert.Equal(t, "foo.com", updateExoscale[0].name) + assert.Equal(t, int64(1), updateExoscale[0].updateDNSRecord.ID) +}