diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index 9a78ffc97c..efe5170376 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -27,6 +27,8 @@ import ( const ( // RecordTypeA is a RecordType enum value RecordTypeA = "A" + // RecordTypeAAAA is a RecordType enum value + RecordTypeAAAA = "AAAA" // RecordTypeCNAME is a RecordType enum value RecordTypeCNAME = "CNAME" // RecordTypeTXT is a RecordType enum value @@ -128,7 +130,7 @@ type Endpoint struct { DNSName string `json:"dnsName,omitempty"` // The targets the DNS record points to Targets Targets `json:"targets,omitempty"` - // RecordType type of record, e.g. CNAME, A, SRV, TXT etc + // RecordType type of record, e.g. CNAME, A, AAAA, SRV, TXT etc RecordType string `json:"recordType,omitempty"` // Identifier to distinguish multiple records with the same name and type (e.g. Route53 records with routing policies other than 'simple') SetIdentifier string `json:"setIdentifier,omitempty"` diff --git a/endpoint/endpoint_test.go b/endpoint/endpoint_test.go index 05104adbf7..80b7cb04e0 100644 --- a/endpoint/endpoint_test.go +++ b/endpoint/endpoint_test.go @@ -35,6 +35,22 @@ func TestNewEndpoint(t *testing.T) { } } +func TestNewEndpointWithIPv6(t *testing.T) { + e := NewEndpoint("example.org", "AAAA", "foo.com") + if e.DNSName != "example.com" || e.Targets[0] != "foo.com" || e.RecordType != "AAAA" { + t.Error("Endpoint is not initialized correctly") + } + + if e.Labels == nil { + t.Error("Labels is not initialized") + } + + w := NewEndpoint("example.org", "", "load-balancer.com.") + if w.DNSName != "example.org" || e.Targets[0] != "load-balancer.com" || w.RecordType != "" { + t.Error("Endpoint is not initialized correctly") + } +} + func TestTargetsSame(t *testing.T) { tests := []Targets{ {""}, diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 5b86a42404..d4d8969e47 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -294,7 +294,7 @@ var defaultConfig = &Config{ TransIPAccountName: "", TransIPPrivateKeyFile: "", DigitalOceanAPIPageSize: 50, - ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, + ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, GoDaddyAPIKey: "", GoDaddySecretKey: "", GoDaddyTTL: 600, @@ -379,7 +379,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("crd-source-apiversion", "API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source").Default(defaultConfig.CRDSourceAPIVersion).StringVar(&cfg.CRDSourceAPIVersion) app.Flag("crd-source-kind", "Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion").Default(defaultConfig.CRDSourceKind).StringVar(&cfg.CRDSourceKind) app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter) - app.Flag("managed-record-types", "Comma separated list of record types to manage (default: A, CNAME) (supported records: CNAME, A, NS").Default("A", "CNAME").StringsVar(&cfg.ManagedDNSRecordTypes) + app.Flag("managed-record-types", "Comma separated list of record types to manage (default: A, AAAA, CNAME) (supported records: CNAME, A, AAAA, NS").Default("A", "AAAA", "CNAME").StringsVar(&cfg.ManagedDNSRecordTypes) app.Flag("default-targets", "Set globally default IP address that will apply as a target instead of source addresses. Specify multiple times for multiple targets (optional)").StringsVar(&cfg.DefaultTargets) // Flags related to providers diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 5266b9b28e..a433abcf04 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -113,7 +113,7 @@ var ( TransIPAccountName: "", TransIPPrivateKeyFile: "", DigitalOceanAPIPageSize: 50, - ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, + ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, RFC2136BatchChangeSize: 50, } @@ -208,7 +208,7 @@ var ( TransIPAccountName: "transip", TransIPPrivateKeyFile: "/path/to/transip.key", DigitalOceanAPIPageSize: 100, - ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, + ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, RFC2136BatchChangeSize: 100, } ) diff --git a/plan/plan.go b/plan/plan.go index 30ee399314..1e1cf35311 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -76,12 +76,12 @@ bar.com | | [->191.1.1.1, ->190.1.1.1] | = create (bar.com -> 1 "=", i.e. result of calculation relies on supplied ConflictResolver */ type planTable struct { - rows map[string]map[string]*planTableRow + rows map[string]map[string]map[string]*planTableRow resolver ConflictResolver } func newPlanTable() planTable { //TODO: make resolver configurable - return planTable{map[string]map[string]*planTableRow{}, PerResource{}} + return planTable{map[string]map[string]map[string]*planTableRow{}, PerResource{}} } // planTableRow @@ -99,23 +99,29 @@ func (t planTableRow) String() string { func (t planTable) addCurrent(e *endpoint.Endpoint) { dnsName := normalizeDNSName(e.DNSName) if _, ok := t.rows[dnsName]; !ok { - t.rows[dnsName] = make(map[string]*planTableRow) + t.rows[dnsName] = make(map[string]map[string]*planTableRow) } if _, ok := t.rows[dnsName][e.SetIdentifier]; !ok { - t.rows[dnsName][e.SetIdentifier] = &planTableRow{} + t.rows[dnsName][e.SetIdentifier] = make(map[string]*planTableRow) } - t.rows[dnsName][e.SetIdentifier].current = e + if _, ok := t.rows[e.SetIdentifier][e.RecordType]; !ok { + t.rows[dnsName][e.SetIdentifier][e.RecordType] = &planTableRow{} + } + t.rows[dnsName][e.SetIdentifier][e.RecordType].current = e } func (t planTable) addCandidate(e *endpoint.Endpoint) { dnsName := normalizeDNSName(e.DNSName) if _, ok := t.rows[dnsName]; !ok { - t.rows[dnsName] = make(map[string]*planTableRow) + t.rows[dnsName] = make(map[string]map[string]*planTableRow) } if _, ok := t.rows[dnsName][e.SetIdentifier]; !ok { - t.rows[dnsName][e.SetIdentifier] = &planTableRow{} + t.rows[dnsName][e.SetIdentifier] = make(map[string]*planTableRow) + } + if _, ok := t.rows[e.SetIdentifier][e.RecordType]; !ok { + t.rows[dnsName][e.SetIdentifier][e.RecordType] = &planTableRow{} } - t.rows[dnsName][e.SetIdentifier].candidates = append(t.rows[dnsName][e.SetIdentifier].candidates, e) + t.rows[dnsName][e.SetIdentifier][e.RecordType].candidates = append(t.rows[dnsName][e.SetIdentifier][e.RecordType].candidates, e) } func (c *Changes) HasChanges() bool { @@ -145,24 +151,26 @@ func (p *Plan) Calculate() *Plan { changes := &Changes{} for _, topRow := range t.rows { - for _, row := range topRow { - if row.current == nil { //dns name not taken - changes.Create = append(changes.Create, t.resolver.ResolveCreate(row.candidates)) - } - if row.current != nil && len(row.candidates) == 0 { - changes.Delete = append(changes.Delete, row.current) - } + for _, midRow := range topRow { + for _, row := range midRow { + if row.current == nil { //dns name not taken + changes.Create = append(changes.Create, t.resolver.ResolveCreate(row.candidates)) + } + if row.current != nil && len(row.candidates) == 0 { + changes.Delete = append(changes.Delete, row.current) + } - // TODO: allows record type change, which might not be supported by all dns providers - if row.current != nil && len(row.candidates) > 0 { //dns name is taken - update := t.resolver.ResolveUpdate(row.current, row.candidates) - // compare "update" to "current" to figure out if actual update is required - if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || p.shouldUpdateProviderSpecific(update, row.current) { - inheritOwner(row.current, update) - changes.UpdateNew = append(changes.UpdateNew, update) - changes.UpdateOld = append(changes.UpdateOld, row.current) + // TODO: allows record type change, which might not be supported by all dns providers + if row.current != nil && len(row.candidates) > 0 { //dns name is taken + update := t.resolver.ResolveUpdate(row.current, row.candidates) + // compare "update" to "current" to figure out if actual update is required + if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) || p.shouldUpdateProviderSpecific(update, row.current) { + inheritOwner(row.current, update) + changes.UpdateNew = append(changes.UpdateNew, update) + changes.UpdateOld = append(changes.UpdateOld, row.current) + } + continue } - continue } } } @@ -174,7 +182,7 @@ func (p *Plan) Calculate() *Plan { Current: p.Current, Desired: p.Desired, Changes: changes, - ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, + ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, } return plan diff --git a/plan/plan_test.go b/plan/plan_test.go index d402f0eb22..fe6d23918f 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -35,6 +35,9 @@ type PlanTestSuite struct { fooV2CnameNoLabel *endpoint.Endpoint fooV3CnameSameResource *endpoint.Endpoint fooA5 *endpoint.Endpoint + fooAAAA *endpoint.Endpoint + dsA *endpoint.Endpoint + dsAAAA *endpoint.Endpoint bar127A *endpoint.Endpoint bar127AWithTTL *endpoint.Endpoint bar127AWithProviderSpecificTrue *endpoint.Endpoint @@ -103,6 +106,30 @@ func (suite *PlanTestSuite) SetupTest() { endpoint.ResourceLabelKey: "ingress/default/foo-5", }, } + suite.fooAAAA = &endpoint.Endpoint{ + DNSName: "foo", + Targets: endpoint.Targets{"2001:DB8::1"}, + RecordType: "AAAA", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/foo-AAAA", + }, + } + suite.dsA = &endpoint.Endpoint{ + DNSName: "ds", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: "A", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/ds", + }, + } + suite.dsAAAA = &endpoint.Endpoint{ + DNSName: "ds", + Targets: endpoint.Targets{"1.1.1.1"}, + RecordType: "AAAA", + Labels: map[string]string{ + endpoint.ResourceLabelKey: "ingress/default/ds-AAAAA", + }, + } suite.bar127A = &endpoint.Endpoint{ DNSName: "bar", Targets: endpoint.Targets{"127.0.0.1"}, @@ -526,53 +553,6 @@ func (suite *PlanTestSuite) TestRemoveEndpointWithUpsert() { validateEntries(suite.T(), changes.Delete, expectedDelete) } -//TODO: remove once multiple-target per endpoint is supported -func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceReplace() { - current := []*endpoint.Endpoint{suite.fooV3CnameSameResource, suite.bar192A} - desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV3CnameSameResource} - expectedCreate := []*endpoint.Endpoint{} - expectedUpdateOld := []*endpoint.Endpoint{suite.fooV3CnameSameResource} - expectedUpdateNew := []*endpoint.Endpoint{suite.fooV1Cname} - expectedDelete := []*endpoint.Endpoint{suite.bar192A} - - p := &Plan{ - Policies: []Policy{&SyncPolicy{}}, - Current: current, - Desired: desired, - ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, - } - - changes := p.Calculate().Changes - validateEntries(suite.T(), changes.Create, expectedCreate) - validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) - validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) - validateEntries(suite.T(), changes.Delete, expectedDelete) -} - -//TODO: remove once multiple-target per endpoint is supported -func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceRetain() { - - current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A} - desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV3CnameSameResource} - expectedCreate := []*endpoint.Endpoint{} - expectedUpdateOld := []*endpoint.Endpoint{} - expectedUpdateNew := []*endpoint.Endpoint{} - expectedDelete := []*endpoint.Endpoint{suite.bar192A} - - p := &Plan{ - Policies: []Policy{&SyncPolicy{}}, - Current: current, - Desired: desired, - ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}, - } - - changes := p.Calculate().Changes - validateEntries(suite.T(), changes.Create, expectedCreate) - validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) - validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) - validateEntries(suite.T(), changes.Delete, expectedDelete) -} - func (suite *PlanTestSuite) TestMultipleRecordsSameNameDifferentSetIdentifier() { current := []*endpoint.Endpoint{suite.multiple1} @@ -667,6 +647,39 @@ func (suite *PlanTestSuite) TestDomainFiltersUpdate() { validateEntries(suite.T(), changes.Delete, expectedDelete) } +func (suite *PlanTestSuite) TestAAAARecords() { + + current := []*endpoint.Endpoint{} + desired := []*endpoint.Endpoint{suite.fooAAAA} + expectedCreate := []*endpoint.Endpoint{suite.fooAAAA} + + p := &Plan{ + Policies: []Policy{&SyncPolicy{}}, + Current: current, + Desired: desired, + ManagedRecords: []string{endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, + } + + changes := p.Calculate().Changes + validateEntries(suite.T(), changes.Create, expectedCreate) +} + +func (suite *PlanTestSuite) TestDualStackRecords() { + current := []*endpoint.Endpoint{} + desired := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA} + expectedCreate := []*endpoint.Endpoint{suite.dsA, suite.dsAAAA} + + p := &Plan{ + Policies: []Policy{&SyncPolicy{}}, + Current: current, + Desired: desired, + ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, + } + + changes := p.Calculate().Changes + validateEntries(suite.T(), changes.Create, expectedCreate) +} + func TestPlan(t *testing.T) { suite.Run(t, new(PlanTestSuite)) } diff --git a/provider/pdns/pdns.go b/provider/pdns/pdns.go index cf8132b7fb..161e23907a 100644 --- a/provider/pdns/pdns.go +++ b/provider/pdns/pdns.go @@ -315,7 +315,6 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet if ep.RecordType == "CNAME" { t = provider.EnsureTrailingDot(t) } - records = append(records, pgo.Record{Content: t}) } rrset := pgo.RrSet{ diff --git a/source/service.go b/source/service.go index e49ff2f49d..4d58bbfbec 100644 --- a/source/service.go +++ b/source/service.go @@ -442,6 +442,14 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro DNSName: hostname, } + eaAAAA := &endpoint.Endpoint{ + RecordTTL: ttl, + RecordType: endpoint.RecordTypeAAAA, + Labels: endpoint.NewLabels(), + Targets: make(endpoint.Targets, 0, defaultTargetsCapacity), + DNSName: hostname, + } + epCNAME := &endpoint.Endpoint{ RecordTTL: ttl, RecordType: endpoint.RecordTypeCNAME, @@ -483,6 +491,9 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro if suitableType(t) == endpoint.RecordTypeA { epA.Targets = append(epA.Targets, t) } + if suitableType(t) == endpoint.RecordTypeAAAA { + eaAAAA.Targets = append(eaAAAA.Targets, t) + } if suitableType(t) == endpoint.RecordTypeCNAME { epCNAME.Targets = append(epCNAME.Targets, t) } @@ -491,6 +502,9 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro if len(epA.Targets) > 0 { endpoints = append(endpoints, epA) } + if len(eaAAAA.Targets) > 0 { + endpoints = append(endpoints, eaAAAA) + } if len(epCNAME.Targets) > 0 { endpoints = append(endpoints, epCNAME) } diff --git a/source/service_test.go b/source/service_test.go index 57a2056e8e..ec65815d35 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -185,6 +185,7 @@ func testServiceSourceEndpoints(t *testing.T) { labels map[string]string annotations map[string]string clusterIP string + ipFamilies []v1.IPFamily externalIPs []string lbs []string serviceTypesFilter []string @@ -199,6 +200,7 @@ func testServiceSourceEndpoints(t *testing.T) { svcType: v1.ServiceTypeLoadBalancer, labels: map[string]string{}, annotations: map[string]string{}, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -212,6 +214,7 @@ func testServiceSourceEndpoints(t *testing.T) { ignoreHostnameAnnotation: true, labels: map[string]string{}, annotations: map[string]string{}, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -226,6 +229,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -243,6 +247,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -257,6 +262,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, clusterIP: "1.2.3.4", externalIPs: []string{}, lbs: []string{}, @@ -271,6 +277,7 @@ func testServiceSourceEndpoints(t *testing.T) { fqdnTemplate: "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com", labels: map[string]string{}, annotations: map[string]string{}, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -288,6 +295,7 @@ func testServiceSourceEndpoints(t *testing.T) { ignoreHostnameAnnotation: true, labels: map[string]string{}, annotations: map[string]string{}, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -307,6 +315,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org., bar.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -329,6 +338,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org., bar.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -346,6 +356,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org., bar.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -363,6 +374,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org, bar.example.org", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -380,6 +392,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"lb.example.com"}, // Kubernetes omits the trailing dot serviceTypesFilter: []string{}, @@ -396,6 +409,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org", // Trailing dot is omitted }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4", "lb.example.com"}, // Kubernetes omits the trailing dot serviceTypesFilter: []string{}, @@ -414,6 +428,7 @@ func testServiceSourceEndpoints(t *testing.T) { controllerAnnotationKey: controllerAnnotationValue, hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -432,6 +447,7 @@ func testServiceSourceEndpoints(t *testing.T) { controllerAnnotationKey: "some-other-tool", hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -447,6 +463,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -464,6 +481,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -478,6 +496,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -496,6 +515,7 @@ func testServiceSourceEndpoints(t *testing.T) { hostnameAnnotationKey: "foo.example.org.", "service.beta.kubernetes.io/external-traffic": "OnlyLocal", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -514,6 +534,7 @@ func testServiceSourceEndpoints(t *testing.T) { hostnameAnnotationKey: "foo.example.org.", "service.beta.kubernetes.io/external-traffic": "SomethingElse", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -530,6 +551,7 @@ func testServiceSourceEndpoints(t *testing.T) { hostnameAnnotationKey: "foo.example.org.", "service.beta.kubernetes.io/external-traffic": "OnlyLocal", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -547,6 +569,7 @@ func testServiceSourceEndpoints(t *testing.T) { hostnameAnnotationKey: "foo.example.org.", "service.beta.kubernetes.io/external-traffic": "Global", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -565,6 +588,7 @@ func testServiceSourceEndpoints(t *testing.T) { hostnameAnnotationKey: "foo.example.org.", "service.beta.kubernetes.io/external-traffic": "OnlyLocal", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -579,6 +603,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{}, serviceTypesFilter: []string{}, @@ -593,6 +618,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{"10.2.3.4", "11.2.3.4"}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -609,6 +635,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4", "8.8.8.8"}, serviceTypesFilter: []string{}, @@ -625,6 +652,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ "zalando.org/dnsname": "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -640,6 +668,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ "zalando.org/dnsname": "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -656,6 +685,7 @@ func testServiceSourceEndpoints(t *testing.T) { labels: map[string]string{ "dns": "route53", }, + ipFamilies: []v1.IPFamily{"IPv4"}, annotations: map[string]string{ "domainName": "foo.example.org., bar.example.org", }, @@ -677,6 +707,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4", "lb.example.com"}, serviceTypesFilter: []string{}, @@ -695,6 +726,7 @@ func testServiceSourceEndpoints(t *testing.T) { kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org", kopsDNSControllerHostnameAnnotationKey: "foo.example.org., bar.example.org", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -713,6 +745,7 @@ func testServiceSourceEndpoints(t *testing.T) { fqdnTemplate: "{{.Name}}.bar.example.com", labels: map[string]string{}, annotations: map[string]string{}, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4", "elb.com"}, serviceTypesFilter: []string{}, @@ -731,6 +764,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4", "elb.com"}, serviceTypesFilter: []string{}, @@ -750,6 +784,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ "zalando.org/dnsname": "mate.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -765,6 +800,7 @@ func testServiceSourceEndpoints(t *testing.T) { fqdnTemplate: "{{.Calibre}}.bar.example.com", labels: map[string]string{}, annotations: map[string]string{}, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -780,6 +816,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -797,6 +834,7 @@ func testServiceSourceEndpoints(t *testing.T) { hostnameAnnotationKey: "foo.example.org.", ttlAnnotationKey: "foo", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -814,6 +852,7 @@ func testServiceSourceEndpoints(t *testing.T) { hostnameAnnotationKey: "foo.example.org.", ttlAnnotationKey: "10", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -831,6 +870,7 @@ func testServiceSourceEndpoints(t *testing.T) { hostnameAnnotationKey: "foo.example.org.", ttlAnnotationKey: "1m", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -848,6 +888,7 @@ func testServiceSourceEndpoints(t *testing.T) { hostnameAnnotationKey: "foo.example.org.", ttlAnnotationKey: "-10", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{}, @@ -864,6 +905,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{string(v1.ServiceTypeLoadBalancer)}, @@ -880,6 +922,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{string(v1.ServiceTypeLoadBalancer)}, @@ -894,6 +937,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ internalHostnameAnnotationKey: "foo.internal.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, clusterIP: "1.1.1.1", externalIPs: []string{}, lbs: []string{"1.2.3.4"}, @@ -912,6 +956,7 @@ func testServiceSourceEndpoints(t *testing.T) { hostnameAnnotationKey: "foo.example.org.", internalHostnameAnnotationKey: "foo.internal.example.org.", }, + ipFamilies: []v1.IPFamily{"IPv4"}, clusterIP: "1.1.1.1", externalIPs: []string{}, lbs: []string{"1.2.3.4"}, @@ -929,6 +974,7 @@ func testServiceSourceEndpoints(t *testing.T) { labels: map[string]string{ "app": "web-external", }, + ipFamilies: []v1.IPFamily{"IPv4"}, clusterIP: "1.1.1.1", externalIPs: []string{}, lbs: []string{"1.2.3.4"}, @@ -947,6 +993,7 @@ func testServiceSourceEndpoints(t *testing.T) { labels: map[string]string{ "app": "web-external", }, + ipFamilies: []v1.IPFamily{"IPv4"}, clusterIP: "1.1.1.1", externalIPs: []string{}, lbs: []string{"1.2.3.4"}, @@ -965,6 +1012,7 @@ func testServiceSourceEndpoints(t *testing.T) { labels: map[string]string{ "app": "web-internal", }, + ipFamilies: []v1.IPFamily{"IPv4"}, clusterIP: "1.1.1.1", externalIPs: []string{}, lbs: []string{"1.2.3.4"}, @@ -981,6 +1029,7 @@ func testServiceSourceEndpoints(t *testing.T) { labels: map[string]string{ "app": "web-internal", }, + ipFamilies: []v1.IPFamily{"IPv4"}, clusterIP: "1.1.1.1", externalIPs: []string{}, lbs: []string{"1.2.3.4"}, @@ -989,6 +1038,38 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{hostnameAnnotationKey: "annotation.bar.example.com"}, expected: []*endpoint.Endpoint{}, }, + { + title: "dual-stack load-balancer service gets both addresses", + svcNamespace: "testing", + svcName: "foobar", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + ipFamilies: []v1.IPFamily{"IPv4", "IPv6"}, + clusterIP: "1.1.1.2,2001:db8::2", + externalIPs: []string{}, + lbs: []string{"1.1.1.1", "2001:db8::1"}, + serviceTypesFilter: []string{}, + annotations: map[string]string{hostnameAnnotationKey: "foobar.example.org"}, + expected: []*endpoint.Endpoint{ + {DNSName: "foobar.example.org", Targets: endpoint.Targets{"1.1.1.1", "2001:db8::1"}}, + }, + }, + { + title: "IPv6-only load-balancer service gets IPv6 endpoint", + svcNamespace: "testing", + svcName: "foobar-v6", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + ipFamilies: []v1.IPFamily{"IPv6"}, + clusterIP: "2001:db8::1", + externalIPs: []string{}, + lbs: []string{"2001:db8::2"}, + serviceTypesFilter: []string{}, + annotations: map[string]string{hostnameAnnotationKey: "foobar-v6.example.org"}, + expected: []*endpoint.Endpoint{ + {DNSName: "foobar-v6.example.org", Targets: endpoint.Targets{"2001:db8::2"}}, + }, + }, } { tc := tc t.Run(tc.title, func(t *testing.T) { diff --git a/source/source.go b/source/source.go index 91214b5f38..881c8896c2 100644 --- a/source/source.go +++ b/source/source.go @@ -222,8 +222,10 @@ func getTargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targ // suitableType returns the DNS resource record type suitable for the target. // In this case type A for IPs and type CNAME for everything else. func suitableType(target string) string { - if net.ParseIP(target) != nil { + if net.ParseIP(target) != nil && net.ParseIP(target).To4() != nil { return endpoint.RecordTypeA + } else if net.ParseIP(target) != nil && net.ParseIP(target).To16() != nil { + return endpoint.RecordTypeAAAA } return endpoint.RecordTypeCNAME } @@ -233,12 +235,23 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin var endpoints []*endpoint.Endpoint var aTargets endpoint.Targets + var aaaaTargets endpoint.Targets var cnameTargets endpoint.Targets for _, t := range targets { switch suitableType(t) { case endpoint.RecordTypeA: - aTargets = append(aTargets, t) + if !isIPv6String(t) { + aTargets = append(aTargets, t) + } else { + continue + } + case endpoint.RecordTypeAAAA: + if isIPv6String(t) { + aaaaTargets = append(aaaaTargets, t) + } else { + continue + } default: cnameTargets = append(cnameTargets, t) } @@ -257,6 +270,19 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin endpoints = append(endpoints, epA) } + if len(aaaaTargets) > 0 { + epAAAA := &endpoint.Endpoint{ + DNSName: strings.TrimSuffix(hostname, "."), + Targets: aaaaTargets, + RecordTTL: ttl, + RecordType: endpoint.RecordTypeAAAA, + Labels: endpoint.NewLabels(), + ProviderSpecific: providerSpecific, + SetIdentifier: setIdentifier, + } + endpoints = append(endpoints, epAAAA) + } + if len(cnameTargets) > 0 { epCNAME := &endpoint.Endpoint{ DNSName: strings.TrimSuffix(hostname, "."), @@ -269,7 +295,6 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin } endpoints = append(endpoints, epCNAME) } - return endpoints } @@ -331,3 +356,9 @@ func waitForDynamicCacheSync(ctx context.Context, factory dynamicInformerFactory } return nil } + +// isIPv6String returns if ip is IPv6. +func isIPv6String(ip string) bool { + netIP := net.ParseIP(ip) + return netIP != nil && netIP.To4() == nil +} diff --git a/source/source_test.go b/source/source_test.go index 0c31b93ba0..d6befe804e 100644 --- a/source/source_test.go +++ b/source/source_test.go @@ -94,6 +94,7 @@ func TestSuitableType(t *testing.T) { target, recordType, expected string }{ {"8.8.8.8", "", "A"}, + {"2001:db8::1", "", "AAAA"}, {"foo.example.org", "", "CNAME"}, {"bar.eu-central-1.elb.amazonaws.com", "", "CNAME"}, } {