diff --git a/go.mod b/go.mod index d7b624ae47..f99b999ba9 100644 --- a/go.mod +++ b/go.mod @@ -151,6 +151,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/neticdk/tidydns-go v0.0.3 github.com/nxadm/tail v1.4.8 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b // indirect diff --git a/go.sum b/go.sum index 69d3d71f0e..b0bd743709 100644 --- a/go.sum +++ b/go.sum @@ -876,6 +876,8 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uY github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/nesv/go-dynect v0.6.0 h1:Ow/DiSm4LAISwnFku/FITSQHnU6pBvhQMsUE5Gu6Oq4= github.com/nesv/go-dynect v0.6.0/go.mod h1:GHRBRKzTwjAMhosHJQq/KrZaFkXIFyJ5zRE7thGXXrs= +github.com/neticdk/tidydns-go v0.0.3 h1:S/fBb3qzSF1vjhSDdXRo+fFE/7XPZKBUJr1bPVarB5c= +github.com/neticdk/tidydns-go v0.0.3/go.mod h1:EGd1iL3+g67y4zxEybaEV0ZK/LwcVubBhePAx+WLK1E= github.com/nic-at/rc0go v1.1.1 h1:bf2gTwYecJEh7qmnOEuarXKueZn4A8N08U1Uop3K8+s= github.com/nic-at/rc0go v1.1.1/go.mod h1:KEa3H5fmDNXCaXSqOeAZxkKnG/8ggr1OHIG25Ve7fjU= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= diff --git a/main.go b/main.go index f8cd7896f8..ec1434847d 100644 --- a/main.go +++ b/main.go @@ -69,6 +69,7 @@ import ( "sigs.k8s.io/external-dns/provider/safedns" "sigs.k8s.io/external-dns/provider/scaleway" "sigs.k8s.io/external-dns/provider/tencentcloud" + "sigs.k8s.io/external-dns/provider/tidydns" "sigs.k8s.io/external-dns/provider/transip" "sigs.k8s.io/external-dns/provider/ultradns" "sigs.k8s.io/external-dns/provider/vinyldns" @@ -182,6 +183,8 @@ func main() { var p provider.Provider switch cfg.Provider { + case "tidydns": + p, err = tidydns.NewTidyDNSProvider(domainFilter, zoneIDFilter, cfg.TidyDNSEndpoint, cfg.DryRun) case "akamai": p, err = akamai.NewAkamaiProvider( akamai.AkamaiConfig{ diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 28271ed416..1126b3c964 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -205,6 +205,7 @@ type Config struct { PiholeTLSInsecureSkipVerify bool PluralCluster string PluralProvider string + TidyDNSEndpoint string } var defaultConfig = &Config{ @@ -350,7 +351,7 @@ var defaultConfig = &Config{ PiholeTLSInsecureSkipVerify: false, PluralCluster: "", PluralProvider: "", -} + TidyDNSEndpoint: ""} // NewConfig returns new Config object func NewConfig() *Config { @@ -392,6 +393,7 @@ func (cfg *Config) ParseFlags(args []string) error { app := kingpin.New("external-dns", "ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers.\n\nNote that all flags may be replaced with env vars - `--flag` -> `EXTERNAL_DNS_FLAG=1` or `--flag value` -> `EXTERNAL_DNS_FLAG=value`") app.Version(Version) app.DefaultEnvars() + app.Flag("tidydns-endpoint", "Provide the endpoint for the TidyDNS service").Default(defaultConfig.TidyDNSEndpoint).StringVar(&cfg.TidyDNSEndpoint) // Flags related to Kubernetes app.Flag("server", "The Kubernetes API server to connect to (default: auto-detect)").Default(defaultConfig.APIServerURL).StringVar(&cfg.APIServerURL) @@ -441,7 +443,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets) // Flags related to providers - providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "bluecat", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "dyn", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "infoblox", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rcodezero", "rdns", "rfc2136", "safedns", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "vinyldns", "vultr"} + providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "bluecat", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "dyn", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "infoblox", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rcodezero", "rdns", "rfc2136", "safedns", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "vinyldns", "vultr", "tidydns"} app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: "+strings.Join(providers, ", ")+")").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, providers...) 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) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index d9c68480c0..87c01c804f 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -32,7 +32,7 @@ import ( var ( minimalConfig = &Config{ - APIServerURL: "", + APIServerURL: "", TidyDNSEndpoint: "", KubeConfig: "", RequestTimeout: time.Second * 30, ContourLoadBalancerService: "heptio-contour/contour", @@ -132,7 +132,7 @@ var ( } overriddenConfig = &Config{ - APIServerURL: "http://127.0.0.1:8080", + APIServerURL: "http://127.0.0.1:8080", TidyDNSEndpoint: "https://tidy.example.com/index.cgi", KubeConfig: "/some/path", RequestTimeout: time.Second * 77, ContourLoadBalancerService: "heptio-contour-other/contour-other", @@ -262,7 +262,7 @@ func TestParseFlags(t *testing.T) { { title: "override everything via flags", args: []string{ - "--server=http://127.0.0.1:8080", + "--server=http://127.0.0.1:8080", "--tidydns-endpoint=https://tidy.example.com/index.cgi", "--kubeconfig=/some/path", "--request-timeout=77s", "--contour-load-balancer=heptio-contour-other/contour-other", @@ -388,7 +388,7 @@ func TestParseFlags(t *testing.T) { title: "override everything via environment variables", args: []string{}, envVars: map[string]string{ - "EXTERNAL_DNS_SERVER": "http://127.0.0.1:8080", + "EXTERNAL_DNS_SERVER": "http://127.0.0.1:8080", "EXTERNAL_DNS_TIDYDNS_ENDPOINT": "https://tidy.example.com/index.cgi", "EXTERNAL_DNS_KUBECONFIG": "/some/path", "EXTERNAL_DNS_REQUEST_TIMEOUT": "77s", "EXTERNAL_DNS_CONTOUR_LOAD_BALANCER": "heptio-contour-other/contour-other", diff --git a/provider/tidydns/tidydns.go b/provider/tidydns/tidydns.go new file mode 100644 index 0000000000..5ce0d8934b --- /dev/null +++ b/provider/tidydns/tidydns.go @@ -0,0 +1,327 @@ +/* +Copyright 2023 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 tidydns + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + + "github.com/neticdk/tidydns-go/pkg/tidydns" + log "github.com/sirupsen/logrus" + + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" +) + +var ( + errorSkip = fmt.Errorf("skipping record") +) + +type tidyDNSProvider struct { + provider.BaseProvider + domainFilter endpoint.DomainFilter + zoneIDFilter provider.ZoneIDFilter + dryRun bool + client tidydns.TidyDNSClient +} + +type groupKey struct { + name string + rType string +} + +// NewTidyDNSProvider initializes a new Dnsimple based provider +func NewTidyDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, endpoint string, dryRun bool) (provider.Provider, error) { + username := os.Getenv("TIDYDNS_USER") + if len(username) == 0 { + return nil, fmt.Errorf("no tidydns username provided") + } + + password := os.Getenv("TIDYDNS_PASS") + if len(password) == 0 { + return nil, fmt.Errorf("no tidydns password provided") + } + + if len(endpoint) == 0 { + return nil, fmt.Errorf("no tidydns endpoint provided") + } + + provider := &tidyDNSProvider{ + domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, + dryRun: dryRun, + client: tidydns.New(endpoint, username, password), + } + return provider, nil +} + +func (t *tidyDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) { + zones, err := t.client.ListZones(ctx) + if err != nil { + return nil, err + } + + grouped := make(map[groupKey][]*tidydns.RecordInfo) + for _, z := range zones { + if !(t.domainFilter.Match(z.Name) || t.domainFilter.MatchParent(z.Name)) || !t.zoneIDFilter.Match(strconv.Itoa(z.ID)) { + log.Debugf("Skipping zone %d due to zone filter", z.ID) + continue + } + + records, err := t.client.ListRecords(ctx, z.ID) + log.Debugf("Got %d records for zone %d", len(records), z.ID) + if err != nil { + return nil, err + } + + for _, r := range records { + rType := "" + switch r.Type { + case tidydns.RecordTypeA: + rType = "A" + case tidydns.RecordTypeCNAME: + rType = "CNAME" + case tidydns.RecordTypeTXT: + rType = "TXT" + r.Destination = fmt.Sprintf("\"%s\"", r.Destination) // external-dns expects quotation marks around the TXT records + default: + continue + } + + dnsName := r.Name + "." + z.Name + if len(strings.Trim(r.Name, ".")) == 0 { + dnsName = z.Name + } + gId := groupKey{name: dnsName, rType: rType} + _, ok := grouped[gId] + if !ok { + grouped[gId] = make([]*tidydns.RecordInfo, 0) + } + grouped[gId] = append(grouped[gId], r) + } + } + + for k, v := range grouped { + targets := make([]string, 0) + ttl := endpoint.TTL(0) + description := "" + for _, r := range v { + targets = append(targets, r.Destination) + ttl = endpoint.TTL(r.TTL) + description = r.Description + } + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(k.name, k.rType, ttl, targets...).WithSetIdentifier(description)) + } + + log.Debugf("Returning endpoints: %+v", endpoints) + + return endpoints, nil +} + +func (t *tidyDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + if !changes.HasChanges() { + return nil + } + + zones, err := t.client.ListZones(ctx) + if err != nil { + return err + } + + for _, c := range changes.Create { + log.Tracef("Handling creation record %+v", c) + + z := findSuitableZones(zones, c.DNSName) + if z == nil { + log.Debugf("Skipping create of record %s because no hosted zone matching record DNS Name was detected", c.DNSName) + continue + } + + rType := convertRecordType(c.RecordType) + if rType == tidydns.RecordType(-1) { + log.Warnf("Skipping create of record %s because it has unsupported record type %s", c.DNSName, c.RecordType) + continue + } + + infos := createRecordInfo(c, z.Name) + for _, in := range infos { + log.Infof("Create record %s (%s) of type %s in zone %s with target %s", c.DNSName, in.Name, c.RecordType, z.Name, in.Destination) + if !t.dryRun { + _, err = t.client.CreateRecord(ctx, z.ID, in) + if err != nil { + return err + } + } + } + } + + for i, u := range changes.UpdateNew { + log.Tracef("Handling update record %+v", u) + + z := findSuitableZones(zones, u.DNSName) + if z == nil { + log.Debugf("Skipping update of record %s because no hosted zone matching record DNS Name was detected", u.DNSName) + continue + } + + records, err := t.findRecords(ctx, u, z) + if err != nil { + if err == errorSkip { + continue + } else { + return err + } + } + + for _, r := range records { + if r.Description == u.SetIdentifier { + infos := createRecordInfo(u, z.Name) + log.Tracef("infos: %+v", infos) + for j, in := range infos { + if changes.UpdateOld[i].Targets[j] == r.Destination { + log.Infof("Update record %s (%s/%d) of type %s in zone %s with target %s", u.DNSName, in.Name, r.ID, u.RecordType, z.Name, in.Destination) + if !t.dryRun { + err := t.client.UpdateRecord(ctx, z.ID, r.ID, in) + if err != nil { + return err + } + } + } + } + } + } + } + + for _, d := range changes.Delete { + log.Tracef("Handling deletion record %+v", d) + + z := findSuitableZones(zones, d.DNSName) + if z == nil { + log.Debugf("Skipping delete of record %s because no hosted zone matching record DNS Name was detected", d.DNSName) + continue + } + + records, err := t.findRecords(ctx, d, z) + if err != nil { + if err == errorSkip { + continue + } else { + return err + } + } + + for _, r := range records { + if r.Description == d.SetIdentifier { + for _, ta := range d.Targets { + if ta == r.Destination { + log.Infof("Deleting record %s (%s/%d) of type %s in zone %s with target %s", d.DNSName, r.Name, r.ID, d.RecordType, z.Name, ta) + if !t.dryRun { + err := t.client.DeleteRecord(ctx, z.ID, r.ID) + if err != nil { + return err + } + } + } + } + } + } + } + + return nil +} + +// findRecords retrives DNS records matching the given endpoint within the given zone +func (t *tidyDNSProvider) findRecords(ctx context.Context, e *endpoint.Endpoint, z *tidydns.ZoneInfo) ([]*tidydns.RecordInfo, error) { + rType := convertRecordType(e.RecordType) + if rType == tidydns.RecordType(-1) { + log.Warnf("Skipping record %s because it has unsupported record type %s", e.DNSName, e.RecordType) + return nil, errorSkip + } + + hostname := strings.TrimSuffix(strings.TrimSuffix(e.DNSName, z.Name), ".") + records, err := t.client.FindRecord(ctx, z.ID, hostname, rType) + log.Tracef("Found records %d based on hostname %s and type %+v", len(records), hostname, rType) + if err != nil { + return nil, err + } + + for _, r := range records { + if r.Type == tidydns.RecordTypeTXT { + r.Destination = fmt.Sprintf("\"%s\"", r.Destination) // external-dns expects quotation marks around the TXT records + } + } + + return records, nil +} + +// findSuitableZones returns DNS zone matching the longest part of the domain of the hostname +func findSuitableZones(zones []*tidydns.ZoneInfo, hostname string) *tidydns.ZoneInfo { + var zone *tidydns.ZoneInfo + for _, z := range zones { + if strings.HasSuffix(hostname, z.Name) { + if zone == nil || len(z.Name) > len(zone.Name) { + zone = z + } + } + } + return zone +} + +// convertRecordType translates the text record type into Tidy constants +func convertRecordType(rType string) tidydns.RecordType { + switch rType { + case "A": + return tidydns.RecordTypeA + case "CNAME": + return tidydns.RecordTypeCNAME + case "TXT": + return tidydns.RecordTypeTXT + default: + return tidydns.RecordType(-1) + } +} + +// createRecordInfo creates record info structures from endpoint +func createRecordInfo(e *endpoint.Endpoint, zone string) []tidydns.RecordInfo { + rType := convertRecordType(e.RecordType) + + dnsName := strings.TrimSuffix(strings.TrimSuffix(e.DNSName, zone), ".") + if len(dnsName) == 0 { + dnsName = "." + } + + records := make([]tidydns.RecordInfo, 0) + for _, ta := range e.Targets { + if rType == tidydns.RecordTypeTXT { + ta = strings.TrimSuffix(strings.TrimPrefix(ta, "\""), "\"") + } + r := tidydns.RecordInfo{ + Name: dnsName, + Destination: ta, + Type: rType, + TTL: int(e.RecordTTL), + Description: e.SetIdentifier, + } + records = append(records, r) + } + + return records +} diff --git a/provider/tidydns/tidydns_test.go b/provider/tidydns/tidydns_test.go new file mode 100644 index 0000000000..44b696ac3e --- /dev/null +++ b/provider/tidydns/tidydns_test.go @@ -0,0 +1,223 @@ +/* +Copyright 2023 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 tidydns + +import ( + "context" + "os" + "testing" + + "github.com/neticdk/tidydns-go/pkg/tidydns" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" +) + +func TestNewTidyDNSProvider(t *testing.T) { + _ = os.Setenv("TIDYDNS_USER", "user") + _ = os.Setenv("TIDYDNS_PASS", "pass") + _, err := NewTidyDNSProvider(endpoint.NewDomainFilter([]string{"tidydns.com"}), provider.NewZoneIDFilter([]string{"1234"}), "endpoint", true) + assert.NoError(t, err) + + _ = os.Unsetenv("TIDYDNS_USER") + _ = os.Unsetenv("TIDYDNS_PASS") + _, err = NewTidyDNSProvider(endpoint.NewDomainFilter([]string{"tidydns.com"}), provider.NewZoneIDFilter([]string{"1234"}), "endpoint", true) + assert.Error(t, err) +} + +func TestTidyDNSRecords(t *testing.T) { + client := &mockClient{} + client.On("ListZones", mock.AnythingOfType("*context.emptyCtx")).Return(zones, nil) + client.On("ListRecords", mock.AnythingOfType("*context.emptyCtx"), 1).Return(zone1, nil) + client.On("ListRecords", mock.AnythingOfType("*context.emptyCtx"), 2).Return(zone2, nil) + + provider := &tidyDNSProvider{ + client: client, + } + recs, err := provider.Records(context.Background()) + assert.NoError(t, err) + assert.Len(t, recs, 3) + client.AssertExpectations(t) +} + +func TestTidyDNSApplyChanges(t *testing.T) { + log.SetLevel(log.TraceLevel) + + client := &mockClient{} + client.On("ListZones", mock.AnythingOfType("*context.emptyCtx")).Return(zones, nil) + client.On("FindRecord", mock.AnythingOfType("*context.emptyCtx"), 1, "ttl", tidydns.RecordTypeA).Return(ttl, nil) + client.On("FindRecord", mock.AnythingOfType("*context.emptyCtx"), 1, "regular3", tidydns.RecordTypeA).Return(regular3, nil) + client.On("FindRecord", mock.AnythingOfType("*context.emptyCtx"), 1, "regular4", tidydns.RecordTypeA).Return(regular4, nil) + client.On("FindRecord", mock.AnythingOfType("*context.emptyCtx"), 1, "regular5", tidydns.RecordTypeA).Return(regular5, nil) + client.On("CreateRecord", mock.AnythingOfType("*context.emptyCtx"), 1, tidydns.RecordInfo{ID: 0, Type: 0, Name: "regular1", Description: "", Destination: "1.1.1.1", TTL: 0, Status: 0, Location: 0}).Return(1, nil) + client.On("CreateRecord", mock.AnythingOfType("*context.emptyCtx"), 1, tidydns.RecordInfo{ID: 0, Type: 0, Name: "ttl", Description: "", Destination: "1.1.1.1", TTL: 100, Status: 0, Location: 0}).Return(2, nil) + client.On("CreateRecord", mock.AnythingOfType("*context.emptyCtx"), 1, tidydns.RecordInfo{ID: 0, Type: 0, Name: "regular2", Description: "", Destination: "1.1.1.2", TTL: 0, Status: 0, Location: 0}).Return(3, nil) + client.On("UpdateRecord", mock.AnythingOfType("*context.emptyCtx"), 1, 3, tidydns.RecordInfo{ID: 0, Type: 0, Name: "regular3", Description: "", Destination: "1.1.2.2", TTL: 100, Status: 0, Location: 0}).Return(nil) + client.On("UpdateRecord", mock.AnythingOfType("*context.emptyCtx"), 1, 4, tidydns.RecordInfo{ID: 0, Type: 0, Name: "regular4", Description: "", Destination: "1.1.2.2", TTL: 100, Status: 0, Location: 0}).Return(nil) + client.On("DeleteRecord", mock.AnythingOfType("*context.emptyCtx"), 1, 5).Return(nil) + client.On("DeleteRecord", mock.AnythingOfType("*context.emptyCtx"), 1, 42).Return(nil) + + provider := &tidyDNSProvider{ + client: client, + dryRun: false, + } + + changes := &plan.Changes{} + changes.Create = []*endpoint.Endpoint{ + {DNSName: "regular1.tidydns1.com", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: "A"}, + {DNSName: "ttl.tidydns1.com", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: "A", RecordTTL: 100}, + {DNSName: "regular2.tidydns1.com", Targets: endpoint.Targets{"1.1.1.2"}, RecordType: "A"}, + } + changes.UpdateOld = []*endpoint.Endpoint{ + {DNSName: "regular3.tidydns1.com", Targets: endpoint.Targets{"127.0.3.1"}, RecordType: "A"}, + {DNSName: "regular4.tidydns1.com", Targets: endpoint.Targets{"127.0.4.1"}, RecordType: "A"}, + } + changes.UpdateNew = []*endpoint.Endpoint{ + {DNSName: "regular3.tidydns1.com", Targets: endpoint.Targets{"1.1.2.2"}, RecordType: "A", RecordTTL: 100}, + {DNSName: "regular4.tidydns1.com", Targets: endpoint.Targets{"1.1.2.2"}, RecordType: "A", RecordTTL: 100}, + } + changes.Delete = []*endpoint.Endpoint{ + {DNSName: "regular5.tidydns1.com", Targets: endpoint.Targets{"1.1.2.2"}, RecordType: "A", RecordTTL: 100}, + {DNSName: "ttl.tidydns1.com", Targets: endpoint.Targets{"1.1.1.1"}, RecordType: "A", RecordTTL: 100}, + } + + err := provider.ApplyChanges(context.Background(), changes) + assert.NoError(t, err) + client.AssertExpectations(t) +} + +type mockClient struct { + mock.Mock +} + +var ( + zones = []*tidydns.ZoneInfo{ + { + ID: 1, + Name: "tidydns1.com", + }, + { + ID: 2, + Name: "tidydns2.com", + }, + } + + zone1 = []*tidydns.RecordInfo{ + { + ID: 11, + Type: tidydns.RecordTypeA, + Name: "tidy11", + Destination: "127.0.1.1", + }, + { + ID: 11, + Type: tidydns.RecordTypeTXT, + Name: "tidy11", + Destination: "\"heritage=external-dns,external-dns/owner=prod1,external-dns/resource=ingress/namesapce/ingress1\"", + }, + } + + zone2 = []*tidydns.RecordInfo{ + { + ID: 21, + Type: tidydns.RecordTypeA, + Name: "tidy21", + Destination: "127.0.2.1", + }, + } + + ttl = []*tidydns.RecordInfo{ + { + ID: 42, + Type: tidydns.RecordTypeA, + Name: "ttl", + Destination: "1.1.1.1", + }, + } + regular3 = []*tidydns.RecordInfo{ + { + ID: 3, + Type: tidydns.RecordTypeA, + Name: "regular3", + Destination: "127.0.3.1", + }, + } + regular4 = []*tidydns.RecordInfo{ + { + ID: 4, + Type: tidydns.RecordTypeA, + Name: "regular4", + Destination: "127.0.4.1", + }, + } + regular5 = []*tidydns.RecordInfo{ + { + ID: 5, + Type: tidydns.RecordTypeA, + Name: "regular5", + Destination: "1.1.2.2", + }, + } +) + +func (m *mockClient) ListZones(ctx context.Context) ([]*tidydns.ZoneInfo, error) { + args := m.Called(ctx) + return args.Get(0).([]*tidydns.ZoneInfo), args.Error(1) +} + +func (*mockClient) FindZoneID(ctx context.Context, name string) (int, error) { + return 0, nil +} + +func (m *mockClient) CreateRecord(ctx context.Context, zoneID int, info tidydns.RecordInfo) (int, error) { + args := m.Called(ctx, zoneID, info) + return args.Int(0), args.Error(1) +} + +func (m *mockClient) UpdateRecord(ctx context.Context, zoneID int, recordID int, info tidydns.RecordInfo) error { + args := m.Called(ctx, zoneID, recordID, info) + return args.Error(0) +} + +func (m *mockClient) ReadRecord(ctx context.Context, zoneID int, recordID int) (*tidydns.RecordInfo, error) { + args := m.Called(ctx, zoneID, recordID) + if args.Get(0) != nil { + return args.Get(0).(*tidydns.RecordInfo), args.Error(1) + } + return nil, args.Error(1) +} + +func (m *mockClient) FindRecord(ctx context.Context, zoneID int, name string, rType tidydns.RecordType) ([]*tidydns.RecordInfo, error) { + args := m.Called(ctx, zoneID, name, rType) + if args.Get(0) != nil { + return args.Get(0).([]*tidydns.RecordInfo), args.Error(1) + } + return nil, args.Error(1) +} + +func (m *mockClient) ListRecords(ctx context.Context, zoneID int) ([]*tidydns.RecordInfo, error) { + args := m.Called(ctx, zoneID) + return args.Get(0).([]*tidydns.RecordInfo), args.Error(1) +} + +func (m *mockClient) DeleteRecord(ctx context.Context, zoneID int, recordID int) error { + args := m.Called(ctx, zoneID, recordID) + return args.Error(0) +}