diff --git a/pkg/controller/provider/alicloud/access.go b/pkg/controller/provider/alicloud/access.go index 092ad60ca..7dee3a2ab 100644 --- a/pkg/controller/provider/alicloud/access.go +++ b/pkg/controller/provider/alicloud/access.go @@ -165,7 +165,7 @@ func (this *access) DeleteRecord(r raw.Record, zone provider.DNSHostedZone) erro return err } -func (this *access) NewRecord(fqdn, rtype, value string, zone provider.DNSHostedZone, ttl int64) raw.Record { +func (this *access) NewRecord(fqdn, rtype, value string, zone provider.DNSHostedZone, ttl int64) (raw.Record, error) { rr := GetRR(fqdn, zone.Domain()) - return (*Record)(&alidns.Record{RR: rr, Type: rtype, Value: value, DomainName: zone.Domain(), TTL: int(ttl)}) + return (*Record)(&alidns.Record{RR: rr, Type: rtype, Value: value, DomainName: zone.Domain(), TTL: int(ttl)}), nil } diff --git a/pkg/controller/provider/azure/execution.go b/pkg/controller/provider/azure/execution.go index a29abd66d..e9436200b 100644 --- a/pkg/controller/provider/azure/execution.go +++ b/pkg/controller/provider/azure/execution.go @@ -100,6 +100,13 @@ func (exec *Execution) buildMappedRecordSet(name string, rset *dns.RecordSet) (b arecords = append(arecords, azure.ARecord{Ipv4Address: &r.Value}) } properties.ARecords = &arecords + case dns.RS_AAAA: + recordType = azure.AAAA + aaaarecords := []azure.AaaaRecord{} + for _, r := range rset.Records { + aaaarecords = append(aaaarecords, azure.AaaaRecord{Ipv6Address: &r.Value}) + } + properties.AaaaRecords = &aaaarecords case dns.RS_CNAME: recordType = azure.CNAME properties.CnameRecord = &azure.CnameRecord{Cname: &rset.Records[0].Value} diff --git a/pkg/controller/provider/cloudflare/access.go b/pkg/controller/provider/cloudflare/access.go index 0daa98ada..bda8ec25d 100644 --- a/pkg/controller/provider/cloudflare/access.go +++ b/pkg/controller/provider/cloudflare/access.go @@ -117,14 +117,14 @@ func (this *access) DeleteRecord(r raw.Record, zone provider.DNSHostedZone) erro return err } -func (this *access) NewRecord(fqdn, rtype, value string, zone provider.DNSHostedZone, ttl int64) raw.Record { +func (this *access) NewRecord(fqdn, rtype, value string, zone provider.DNSHostedZone, ttl int64) (raw.Record, error) { return (*Record)(&cloudflare.DNSRecord{ Type: rtype, Name: fqdn, Content: value, TTL: int(ttl), ZoneID: zone.Id(), - }) + }), nil } func testTTL(ttl *int) { diff --git a/pkg/controller/provider/infoblox/access.go b/pkg/controller/provider/infoblox/access.go index e5e9eef40..92ebbc660 100644 --- a/pkg/controller/provider/infoblox/access.go +++ b/pkg/controller/provider/infoblox/access.go @@ -18,6 +18,7 @@ package infoblox import ( + "fmt" "strconv" "strings" @@ -62,7 +63,7 @@ func (this *access) DeleteRecord(r raw.Record, zone provider.DNSHostedZone) erro return err } -func (this *access) NewRecord(fqdn string, rtype string, value string, zone provider.DNSHostedZone, ttl int64) (record raw.Record) { +func (this *access) NewRecord(fqdn string, rtype string, value string, zone provider.DNSHostedZone, ttl int64) (record raw.Record, err error) { switch rtype { case dns.RS_A: record = (*RecordA)(ibclient.NewRecordA(ibclient.RecordA{ @@ -71,6 +72,8 @@ func (this *access) NewRecord(fqdn string, rtype string, value string, zone prov //Zone: zone.Key(), View: this.view, })) + case dns.RS_AAAA: + err = fmt.Errorf("warning: aaaa records not supported on infoblox yet") case dns.RS_CNAME: record = (*RecordCNAME)(ibclient.NewRecordCNAME(ibclient.RecordCNAME{ Name: fqdn, diff --git a/pkg/controller/provider/netlify/access.go b/pkg/controller/provider/netlify/access.go index c57244841..33a64dbb3 100644 --- a/pkg/controller/provider/netlify/access.go +++ b/pkg/controller/provider/netlify/access.go @@ -128,14 +128,14 @@ func (this *access) DeleteRecord(r raw.Record, zone provider.DNSHostedZone) erro return err } -func (this *access) NewRecord(fqdn, rtype, value string, zone provider.DNSHostedZone, ttl int64) raw.Record { +func (this *access) NewRecord(fqdn, rtype, value string, zone provider.DNSHostedZone, ttl int64) (raw.Record, error) { return (*Record)(&models.DNSRecord{ Type: rtype, Hostname: fqdn, Value: value, TTL: int64(ttl), DNSZoneID: zone.Id(), - }) + }), nil } func testTTL(ttl *int) { diff --git a/pkg/controller/provider/openstack/handler.go b/pkg/controller/provider/openstack/handler.go index ae5218be5..ed4aa172e 100644 --- a/pkg/controller/provider/openstack/handler.go +++ b/pkg/controller/provider/openstack/handler.go @@ -201,7 +201,7 @@ func (h *Handler) getZoneState(zone provider.DNSHostedZone, cache provider.ZoneC recordSetHandler := func(recordSet *recordsets.RecordSet) error { switch recordSet.Type { - case dns.RS_A, dns.RS_CNAME, dns.RS_TXT: + case dns.RS_A, dns.RS_AAAA, dns.RS_CNAME, dns.RS_TXT: rs := dns.NewRecordSet(recordSet.Type, int64(recordSet.TTL), nil) for _, record := range recordSet.Records { value := record diff --git a/pkg/dns/provider/changemodel.go b/pkg/dns/provider/changemodel.go index 673103c4d..b7b98cac1 100644 --- a/pkg/dns/provider/changemodel.go +++ b/pkg/dns/provider/changemodel.go @@ -463,15 +463,19 @@ func (this *ChangeModel) AddTargets(set *dns.DNSSet, base *dns.DNSSet, provider ttl := t.GetEntry().TTL() if t.GetRecordType() == dns.RS_CNAME && len(targets) > 1 { cnames = append(cnames, t.GetHostName()) - addrs, err := lookupHostIPv4(t.GetHostName()) + ipv4addrs, ipv6addrs, err := lookupHosts(t.GetHostName()) if err == nil { - for _, addr := range addrs { + for _, addr := range ipv4addrs { AddRecord(targetsets, dns.RS_A, addr, ttl) } + for _, addr := range ipv6addrs { + AddRecord(targetsets, dns.RS_AAAA, addr, ttl) + } } else { this.Errorf("cannot lookup '%s': %s", t.GetHostName(), err) } - this.Debugf("mapping target '%s' to A records: %s", t.GetHostName(), strings.Join(addrs, ",")) + this.Debugf("mapping target '%s' to A records: %s or AAAA records: %s", + t.GetHostName(), strings.Join(ipv4addrs, ","), strings.Join(ipv6addrs, ",")) } else { t = provider.MapTarget(t) AddRecord(targetsets, t.GetRecordType(), t.GetHostName(), ttl) diff --git a/pkg/dns/provider/entry.go b/pkg/dns/provider/entry.go index 00d437bf7..49ce210cf 100644 --- a/pkg/dns/provider/entry.go +++ b/pkg/dns/provider/entry.go @@ -319,8 +319,10 @@ func validate(logger logger.LogContext, state *state, entry *EntryVersion, p *En } var new Target new, err = NewTargetFromEntryVersion(t, entry) + // we should ignore unsupported addresses since the user might not influence + // the provider assigned ip address type if err != nil { - return + continue } if targets.Has(new) { warnings = append(warnings, fmt.Sprintf("dns entry %q has duplicate target %q", entry.ObjectName(), new)) @@ -655,17 +657,26 @@ func normalizeTargets(logger logger.LogContext, object *dnsutils.DNSEntryObject, return result, true, false } for _, t := range targets { - addrs, err := lookupHostIPv4(t.GetHostName()) + ipv4addrs, ipv6addrs, err := lookupHosts(t.GetHostName()) if err == nil { - outer: - for _, addr := range addrs { + outerV4: + for _, addr := range ipv4addrs { for _, old := range result { if old.GetHostName() == addr { - continue outer + continue outerV4 } } result = append(result, NewTarget(dns.RS_A, addr, t.GetEntry())) } + outerV6: + for _, addr := range ipv6addrs { + for _, old := range result { + if old.GetHostName() == addr { + continue outerV6 + } + } + result = append(result, NewTarget(dns.RS_AAAA, addr, t.GetEntry())) + } } else { w := fmt.Sprintf("cannot lookup '%s': %s", t.GetHostName(), err) logger.Warn(w) @@ -675,22 +686,24 @@ func normalizeTargets(logger logger.LogContext, object *dnsutils.DNSEntryObject, return result, true, true } -func lookupHostIPv4(hostname string) ([]string, error) { +func lookupHosts(hostname string) ([]string, []string, error) { ips, err := net.LookupIP(hostname) if err != nil { - return nil, err + return nil, nil, err } - addrs := make([]string, 0, len(ips)) + ipv4addrs := make([]string, 0, len(ips)) + ipv6addrs := make([]string, 0, len(ips)) for _, ip := range ips { - if ip.To4() == nil { - continue + if ip.To4() != nil { + ipv4addrs = append(ipv4addrs, ip.String()) + } else if ip.To16() != nil { + ipv6addrs = append(ipv6addrs, ip.String()) } - addrs = append(addrs, ip.String()) } - if len(addrs) == 0 { - return nil, fmt.Errorf("%s has no IPv4 address (of %d addresses)", hostname, len(ips)) + if len(ipv4addrs) == 0 && len(ipv6addrs) == 0 { + return nil, nil, fmt.Errorf("%s has no IPv4/IPv6 address (of %d addresses)", hostname, len(ips)) } - return addrs, nil + return ipv4addrs, ipv6addrs, nil } /////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/dns/provider/raw/execution.go b/pkg/dns/provider/raw/execution.go index d61130af8..835a7f584 100644 --- a/pkg/dns/provider/raw/execution.go +++ b/pkg/dns/provider/raw/execution.go @@ -30,7 +30,7 @@ type Executor interface { UpdateRecord(r Record, zone provider.DNSHostedZone) error DeleteRecord(r Record, zone provider.DNSHostedZone) error - NewRecord(fqdn, rtype, value string, zone provider.DNSHostedZone, ttl int64) Record + NewRecord(fqdn, rtype, value string, zone provider.DNSHostedZone, ttl int64) (Record, error) } type result struct { @@ -119,7 +119,12 @@ func (this *Execution) add(dnsname string, rset *dns.RecordSet, modonly bool, fo } } else { if notfound != nil { - *notfound = append(*notfound, this.executor.NewRecord(dnsname, rset.Type, r.Value, this.zone, rset.TTL)) + record, err := this.executor.NewRecord(dnsname, rset.Type, r.Value, this.zone, rset.TTL) + if err == nil { + *notfound = append(*notfound, record) + } else { + this.Errorf("Error adding record: %s (%s): %s", dnsname, rtype, err) + } } } } diff --git a/pkg/dns/provider/target.go b/pkg/dns/provider/target.go index 38d8b5550..7e5331e16 100644 --- a/pkg/dns/provider/target.go +++ b/pkg/dns/provider/target.go @@ -18,8 +18,9 @@ package provider import ( "fmt" - "github.com/gardener/external-dns-management/pkg/dns" "net" + + "github.com/gardener/external-dns-management/pkg/dns" ) //////////////////////////////////////////////////////////////////////////////// @@ -77,6 +78,8 @@ func NewTargetFromEntryVersion(name string, entry *EntryVersion) (Target, error) return NewTarget(dns.RS_CNAME, name, entry), nil } else if ip.To4() != nil { return NewTarget(dns.RS_A, name, entry), nil + } else if ip.To16() != nil { + return NewTarget(dns.RS_AAAA, name, entry), nil } else { return nil, fmt.Errorf("IPv6 addresses are not supported yet: %s (%s)", ip.String(), name) } diff --git a/pkg/dns/records.go b/pkg/dns/records.go index bcdcc72b1..846097f87 100644 --- a/pkg/dns/records.go +++ b/pkg/dns/records.go @@ -27,6 +27,7 @@ const RS_ALIAS = "ALIAS" // provider specific alias for CNAME record (e.g. AWS a const RS_TXT = "TXT" const RS_CNAME = "CNAME" const RS_A = "A" +const RS_AAAA = "AAAA" const RS_NS = "NS" diff --git a/pkg/dns/utils.go b/pkg/dns/utils.go index 6e70cd000..b1922fdf4 100644 --- a/pkg/dns/utils.go +++ b/pkg/dns/utils.go @@ -23,7 +23,7 @@ import ( func SupportedRecordType(t string) bool { switch t { - case RS_CNAME, RS_A, RS_TXT: + case RS_CNAME, RS_A, RS_AAAA, RS_TXT: return true } return false diff --git a/test/functional/basics.go b/test/functional/basics.go index f153f27bb..8c582babf 100644 --- a/test/functional/basics.go +++ b/test/functional/basics.go @@ -68,6 +68,29 @@ spec: --- apiVersion: dns.gardener.cloud/v1alpha1 kind: DNSEntry +metadata: + name: {{.Prefix}}aaaa + namespace: {{.Namespace}} +spec: + dnsName: {{.Prefix}}aaaa.{{.Domain}} + ttl: {{.TTL}} + targets: + - 20a0::1 +--- +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry +metadata: + name: {{.Prefix}}mixed + namespace: {{.Namespace}} +spec: + dnsName: {{.Prefix}}mixed.{{.Domain}} + ttl: {{.TTL}} + targets: + - 20a0::2 + - 11.11.0.11 +--- +apiVersion: dns.gardener.cloud/v1alpha1 +kind: DNSEntry metadata: name: {{.Prefix}}txt namespace: {{.Namespace}} @@ -168,7 +191,7 @@ func functestbasics(cfg *config.Config, p *config.ProviderConfig) { Ω(err).Should(BeNil()) entryNames := []string{} - for _, name := range []string{"a", "txt", "wildcard", "cname", "cname-multi"} { + for _, name := range []string{"a", "aaaa", "mixed", "txt", "wildcard", "cname", "cname-multi"} { entryNames = append(entryNames, entryName(p, name)) } if p.AliasTarget != "" { @@ -199,6 +222,42 @@ func functestbasics(cfg *config.Config, p *config.ProviderConfig) { "zone": Equal(p.ZoneID), }), }), + entryName(p, "aaaa"): MatchKeys(IgnoreExtras, Keys{ + "metadata": MatchKeys(IgnoreExtras, Keys{ + "finalizers": And(HaveLen(1), ContainElement("dns.gardener.cloud/"+p.FinalizerType)), + }), + "spec": MatchKeys(IgnoreExtras, Keys{ + "dnsName": Equal(dnsName(p, "aaaa")), + "targets": And(HaveLen(1), ContainElement("20a0::2"), ContainElement("20a0::2")), + }), + "status": MatchKeys(IgnoreExtras, Keys{ + "message": Equal("dns entry active"), + "provider": Equal(p.Namespace + "/" + p.Name), + "providerType": Equal(p.Type), + "state": Equal("Ready"), + "targets": And(HaveLen(1), ContainElement("20a0::1")), + "ttl": Equal(float64(ttl)), + "zone": Equal(p.ZoneID), + }), + }), + entryName(p, "mixed"): MatchKeys(IgnoreExtras, Keys{ + "metadata": MatchKeys(IgnoreExtras, Keys{ + "finalizers": And(HaveLen(1), ContainElement("dns.gardener.cloud/"+p.FinalizerType)), + }), + "spec": MatchKeys(IgnoreExtras, Keys{ + "dnsName": Equal(dnsName(p, "aaaa")), + "targets": And(HaveLen(2), ContainElement("20a0::2"), ContainElement("11.11.0.11")), + }), + "status": MatchKeys(IgnoreExtras, Keys{ + "message": Equal("dns entry active"), + "provider": Equal(p.Namespace + "/" + p.Name), + "providerType": Equal(p.Type), + "state": Equal("Ready"), + "targets": And(HaveLen(2), ContainElement("20a0::2"), ContainElement("11.11.0.11")), + "ttl": Equal(float64(ttl)), + "zone": Equal(p.ZoneID), + }), + }), entryName(p, "txt"): MatchKeys(IgnoreExtras, Keys{ "spec": MatchKeys(IgnoreExtras, Keys{ "dnsName": Equal(dnsName(p, "txt")), @@ -272,6 +331,8 @@ func functestbasics(cfg *config.Config, p *config.ProviderConfig) { u.AwaitLookupCName(dnsName(p, "alias"), p.AliasTarget) } u.AwaitLookup(dnsName(p, "a"), "11.11.11.11") + u.AwaitLookup(dnsName(p, "aaaa"), "20a0::1") + u.AwaitLookup(dnsName(p, "mixed"), "20a0::2", "11.11.0.11") randname := config.RandStringBytes(6) u.AwaitLookup(randname+"."+dnsName(p, "wildcard"), "44.44.44.44") u.AwaitLookupCName(dnsName(p, "cname"), "google-public-dns-a.google.com")