diff --git a/consumers/aws.go b/consumers/aws.go index 0d8d4e4..65db631 100644 --- a/consumers/aws.go +++ b/consumers/aws.go @@ -31,6 +31,7 @@ type awsConsumer struct { const ( evaluateTargetHealth = true defaultTxtTTL = int64(300) + defaultATTL = int64(300) ) // NewAWSRoute53Consumer reates a Consumer instance to sync and process DNS @@ -50,7 +51,7 @@ func withClient(c AWSClient, groupID string) *awsConsumer { } func (a *awsConsumer) Sync(endpoints []*pkg.Endpoint) error { - kubeRecords, err := a.endpointsToAlias(endpoints) + kubeRecords, err := a.endpointsToRecords(endpoints) if err != nil { return err } @@ -68,7 +69,7 @@ func (a *awsConsumer) Sync(endpoints []*pkg.Endpoint) error { for _, record := range kubeRecords { zoneID := getZoneIDForEndpoint(hostedZonesMap, record) //this guarantees that the endpoint will not be created in multiple hosted zones if zoneID == "" { - log.Warnf("Hosted zone for endpoint: %s is not found. Skipping record...", aws.StringValue(record.Name)) + log.Warnf("Hosted zone for endpoint: %s was not found. Skipping record...", aws.StringValue(record.Name)) continue } inputByZoneID[zoneID] = append(inputByZoneID[zoneID], record) @@ -103,11 +104,11 @@ func (a *awsConsumer) syncPerHostedZone(kubeRecords []*route53.ResourceRecordSet upsertedMap := make(map[string]bool) // keep track of records to be upserted targetMap := map[string][]*string{} // map dnsname -> list of targets for _, kr := range kubeRecords { - targetMap[aws.StringValue(kr.Name)] = append(targetMap[aws.StringValue(kr.Name)], kr.AliasTarget.DNSName) + targetMap[aws.StringValue(kr.Name)] = append(targetMap[aws.StringValue(kr.Name)], aws.String(a.getRecordTarget(kr))) } //find records to be upserted for _, kubeRecord := range kubeRecords { - //make sure that another record with same DNS name is not already included into upsert slice + //make sure that another record with same DNS name was not already included into upsert slice if _, upserted := upsertedMap[aws.StringValue(kubeRecord.Name)]; upserted { continue } @@ -202,19 +203,19 @@ func (a *awsConsumer) Process(endpoint *pkg.Endpoint) error { return err } - aliasRecords, err := a.endpointsToAlias([]*pkg.Endpoint{endpoint}) + ARecords, err := a.endpointsToRecords([]*pkg.Endpoint{endpoint}) if err != nil { return err } - if len(aliasRecords) != 1 { - return fmt.Errorf("Failed to process endpoint. Alias could not be constructed for: %s:%s.", endpoint.DNSName, endpoint.Hostname) + if len(ARecords) != 1 { + return fmt.Errorf("failed to process endpoint. A record could not be constructed for: %s:%s:%s", endpoint.DNSName, endpoint.Hostname, endpoint.IP) } - create := []*route53.ResourceRecordSet{aliasRecords[0], a.getAssignedTXTRecordObject(aliasRecords[0])} + create := []*route53.ResourceRecordSet{ARecords[0], a.getAssignedTXTRecordObject(ARecords[0])} - zoneID := getZoneIDForEndpoint(hostedZonesMap, aliasRecords[0]) + zoneID := getZoneIDForEndpoint(hostedZonesMap, ARecords[0]) if zoneID == "" { - log.Warnf("Hosted zone for endpoint: %s is not found. Skipping record...", endpoint.DNSName) + log.Warnf("Hosted zone for endpoint: %s was not found. Skipping record...", endpoint.DNSName) return nil } @@ -282,7 +283,7 @@ func (a *awsConsumer) recordInfo(records []*route53.ResourceRecordSet) map[strin } } if aws.StringValue(record.Type) != "TXT" { - infoMap[aws.StringValue(record.Name)].Target = pkg.SanitizeDNSName(a.getRecordTarget(record)) + infoMap[aws.StringValue(record.Name)].Target = a.getRecordTarget(record) //sanitization not needed here, as per IP case } } @@ -300,11 +301,13 @@ func (a *awsConsumer) getRecordTarget(r *route53.ResourceRecordSet) string { return aws.StringValue(r.ResourceRecords[0].Value) } -//endpointsToAlias converts pkg Endpoint to route53 Alias Records -func (a *awsConsumer) endpointsToAlias(endpoints []*pkg.Endpoint) ([]*route53.ResourceRecordSet, error) { - lbDNS := make([]string, len(endpoints)) - for i := range endpoints { - lbDNS[i] = endpoints[i].Hostname +//endpointsToRecords converts pkg Endpoint to route53 A [Alias] Records depending whether IP/LB Hostname is used +func (a *awsConsumer) endpointsToRecords(endpoints []*pkg.Endpoint) ([]*route53.ResourceRecordSet, error) { + lbDNS := make([]string, 0, len(endpoints)) + for _, endpoint := range endpoints { + if endpoint.Hostname != "" { + lbDNS = append(lbDNS, endpoint.Hostname) + } } zoneIDs, err := a.client.GetCanonicalZoneIDs(lbDNS) if err != nil { @@ -314,24 +317,36 @@ func (a *awsConsumer) endpointsToAlias(endpoints []*pkg.Endpoint) ([]*route53.Re for _, ep := range endpoints { if loadBalancerZoneID, exist := zoneIDs[ep.Hostname]; exist { - rset = append(rset, a.endpointToAlias(ep, aws.String(loadBalancerZoneID))) + rset = append(rset, a.endpointToRecord(ep, aws.String(loadBalancerZoneID))) + } else if ep.IP != "" { + rset = append(rset, a.endpointToRecord(ep, aws.String(""))) } else { - log.Errorf("Canonical Zone ID for endpoint: %s is not found", ep.Hostname) + log.Errorf("Canonical Zone ID for endpoint: %s was not found", ep.Hostname) } } return rset, nil } -//endpointToAlias convert endpoint to an AWS A Alias record -func (a *awsConsumer) endpointToAlias(ep *pkg.Endpoint, canonicalZoneID *string) *route53.ResourceRecordSet { +//endpointToRecord convert endpoint to an AWS A [Alias] record depending whether IP of LB hostname is used +//if both are specified hostname takes precedence and Alias record is to be created +func (a *awsConsumer) endpointToRecord(ep *pkg.Endpoint, canonicalZoneID *string) *route53.ResourceRecordSet { rs := &route53.ResourceRecordSet{ Type: aws.String("A"), Name: aws.String(pkg.SanitizeDNSName(ep.DNSName)), - AliasTarget: &route53.AliasTarget{ + } + if ep.Hostname != "" { + rs.AliasTarget = &route53.AliasTarget{ DNSName: aws.String(pkg.SanitizeDNSName(ep.Hostname)), EvaluateTargetHealth: aws.Bool(evaluateTargetHealth), HostedZoneId: canonicalZoneID, - }, + } + } else { + rs.TTL = aws.Int64(defaultATTL) + rs.ResourceRecords = []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String(ep.IP), + }, + } } return rs } diff --git a/consumers/aws_test.go b/consumers/aws_test.go index c13322a..33c58fe 100644 --- a/consumers/aws_test.go +++ b/consumers/aws_test.go @@ -21,23 +21,45 @@ type awsTestItem struct { expectFail bool } -func TestEndpointToAlias(t *testing.T) { +func TestEndpointToRecord(t *testing.T) { groupID := "test" zoneID := "test" client := &awsConsumer{ groupID: groupID, } + //both Hostname and IP specified -> Alias Record ep := &pkg.Endpoint{ DNSName: "example.com", IP: "10.202.10.123", Hostname: "amazon.elb.com", } - rsA := client.endpointToAlias(ep, &zoneID) + rsA := client.endpointToRecord(ep, &zoneID) if *rsA.Type != "A" || *rsA.Name != pkg.SanitizeDNSName(ep.DNSName) || *rsA.AliasTarget.DNSName != pkg.SanitizeDNSName(ep.Hostname) || *rsA.AliasTarget.HostedZoneId != zoneID { + t.Error("Should create an Alias A record") + } + // only IP specified -> plain A Record + ep = &pkg.Endpoint{ + DNSName: "example.com", + IP: "10.202.10.123", + } + rsA = client.endpointToRecord(ep, &zoneID) + if *rsA.Type != "A" || *rsA.Name != pkg.SanitizeDNSName(ep.DNSName) || + len(rsA.ResourceRecords) != 1 || *rsA.ResourceRecords[0].Value != ep.IP { t.Error("Should create an A record") } + //only Hostname specified -> Alias Record + ep = &pkg.Endpoint{ + DNSName: "example.com", + Hostname: "amazon.elb.com", + } + rsA = client.endpointToRecord(ep, &zoneID) + if *rsA.Type != "A" || *rsA.Name != pkg.SanitizeDNSName(ep.DNSName) || + *rsA.AliasTarget.DNSName != pkg.SanitizeDNSName(ep.Hostname) || + *rsA.AliasTarget.HostedZoneId != zoneID { + t.Error("Should create an Alias A record") + } } func TestGetAssignedTXTRecordObject(t *testing.T) { @@ -51,7 +73,7 @@ func TestGetAssignedTXTRecordObject(t *testing.T) { IP: "10.202.10.123", Hostname: "amazon.elb.com", } - rsA := client.endpointToAlias(ep, &zoneID) + rsA := client.endpointToRecord(ep, &zoneID) rsTXT := client.getAssignedTXTRecordObject(rsA) if *rsTXT.Type != "TXT" || *rsTXT.Name != "example.com." || @@ -115,7 +137,7 @@ func TestRecordInfo(t *testing.T) { Type: aws.String("A"), Name: aws.String("test.example.com."), AliasTarget: &route53.AliasTarget{ - DNSName: aws.String("abc.def.ghi"), + DNSName: aws.String("abc.def.ghi."), HostedZoneId: aws.String("123"), }, }, @@ -143,6 +165,40 @@ func TestRecordInfo(t *testing.T) { t.Errorf("Incorrect record info for %v", records) } } + records = []*route53.ResourceRecordSet{ + &route53.ResourceRecordSet{ + Type: aws.String("A"), + Name: aws.String("test.example.com."), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String("54.32.12.32"), + }, + }, + }, + &route53.ResourceRecordSet{ + Type: aws.String("TXT"), + Name: aws.String("test.example.com."), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String(client.getGroupID()), + }, + }, + }, + } + recordInfoMap = client.recordInfo(records) + if len(recordInfoMap) != 1 { + t.Errorf("Incorrect record info for %v", records) + } + if val, exist := recordInfoMap["test.example.com."]; !exist { + t.Errorf("Incorrect record info for %v", records) + } else { + if val.GroupID != client.getGroupID() { + t.Errorf("Incorrect record info for %v", records) + } + if !sameTargets("54.32.12.32", val.Target) { + t.Errorf("Incorrect record target for %v", records) + } + } records = []*route53.ResourceRecordSet{ &route53.ResourceRecordSet{ Type: aws.String("TXT"), @@ -174,7 +230,7 @@ func TestRecordInfo(t *testing.T) { Type: aws.String("A"), Name: aws.String("new.example.com."), AliasTarget: &route53.AliasTarget{ - DNSName: aws.String("elb.com"), + DNSName: aws.String("elb.com."), HostedZoneId: aws.String("123"), }, }, @@ -191,7 +247,7 @@ func TestRecordInfo(t *testing.T) { Type: aws.String("A"), Name: aws.String("test.example.com."), AliasTarget: &route53.AliasTarget{ - DNSName: aws.String("abc.def.ghi"), + DNSName: aws.String("abc.def.ghi."), HostedZoneId: aws.String("123"), }, }, @@ -294,20 +350,17 @@ func checkEndpointSlices(got []*route53.ResourceRecordSet, expect []*route53.Res } var found bool for _, eep := range expect { - if *ep.Type == "A" { + if eep.AliasTarget != nil && ep.AliasTarget != nil { if *eep.Type == "A" && pkg.SanitizeDNSName(*eep.AliasTarget.DNSName) == pkg.SanitizeDNSName(*ep.AliasTarget.DNSName) && *ep.Name == *eep.Name { found = true } - continue - } - if *ep.Type == "TXT" { - if *eep.Type == "TXT" && *ep.Name == *eep.Name { + } else if ep.ResourceRecords != nil && len(ep.ResourceRecords) > 0 && eep.ResourceRecords != nil && len(eep.ResourceRecords) > 0 { + if *eep.Type == "A" && *eep.ResourceRecords[0].Value == *ep.ResourceRecords[0].Value && + *ep.Name == *eep.Name { found = true } - continue } - return false } if !found { return false @@ -383,6 +436,9 @@ func TestAWSConsumer(t *testing.T) { //exclude IP endpoints for now only Alias w { "nest.sub.example.com", "", "nested.elb", }, + { + "ip.sub.example.com", "192.168.0.1", "", + }, }, expectUpsert: map[string][]*route53.ResourceRecordSet{ "sub.example.com.": []*route53.ResourceRecordSet{ @@ -398,6 +454,19 @@ func TestAWSConsumer(t *testing.T) { //exclude IP endpoints for now only Alias w Type: aws.String("TXT"), Name: aws.String("nest.sub.example.com."), }, + &route53.ResourceRecordSet{ + Type: aws.String("A"), + Name: aws.String("ip.sub.example.com."), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String("192.168.0.1"), + }, + }, + }, + &route53.ResourceRecordSet{ + Type: aws.String("TXT"), + Name: aws.String("ip.sub.example.com."), + }, }, "example.com.": []*route53.ResourceRecordSet{ &route53.ResourceRecordSet{ @@ -428,6 +497,19 @@ func TestAWSConsumer(t *testing.T) { //exclude IP endpoints for now only Alias w }, expectDelete: map[string][]*route53.ResourceRecordSet{ "foo.com.": []*route53.ResourceRecordSet{ + &route53.ResourceRecordSet{ + Type: aws.String("A"), + Name: aws.String("public-ip.foo.com."), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String("127.0.0.1"), + }, + }, + }, + &route53.ResourceRecordSet{ + Type: aws.String("TXT"), + Name: aws.String("public-ip.foo.com."), + }, &route53.ResourceRecordSet{ Type: aws.String("A"), Name: aws.String("update.foo.com."), @@ -494,6 +576,19 @@ func TestAWSConsumer(t *testing.T) { //exclude IP endpoints for now only Alias w }, expectDelete: map[string][]*route53.ResourceRecordSet{ "foo.com.": []*route53.ResourceRecordSet{ + &route53.ResourceRecordSet{ + Type: aws.String("A"), + Name: aws.String("public-ip.foo.com."), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String("127.0.0.1"), + }, + }, + }, + &route53.ResourceRecordSet{ + Type: aws.String("TXT"), + Name: aws.String("public-ip.foo.com."), + }, &route53.ResourceRecordSet{ Type: aws.String("A"), Name: aws.String("update.foo.com."), @@ -557,6 +652,19 @@ func TestAWSConsumer(t *testing.T) { //exclude IP endpoints for now only Alias w }, expectDelete: map[string][]*route53.ResourceRecordSet{ "foo.com.": []*route53.ResourceRecordSet{ + &route53.ResourceRecordSet{ + Type: aws.String("A"), + Name: aws.String("public-ip.foo.com."), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String("127.0.0.1"), + }, + }, + }, + &route53.ResourceRecordSet{ + Type: aws.String("TXT"), + Name: aws.String("public-ip.foo.com."), + }, &route53.ResourceRecordSet{ Type: aws.String("A"), Name: aws.String("update.foo.com."), @@ -611,6 +719,19 @@ func TestAWSConsumer(t *testing.T) { //exclude IP endpoints for now only Alias w }, }, "foo.com.": []*route53.ResourceRecordSet{ + &route53.ResourceRecordSet{ + Type: aws.String("A"), + Name: aws.String("public-ip.foo.com."), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String("127.0.0.1"), + }, + }, + }, + &route53.ResourceRecordSet{ + Type: aws.String("TXT"), + Name: aws.String("public-ip.foo.com."), + }, &route53.ResourceRecordSet{ Type: aws.String("A"), Name: aws.String("update.foo.com."), @@ -661,6 +782,19 @@ func TestAWSConsumer(t *testing.T) { //exclude IP endpoints for now only Alias w }, }, "foo.com.": []*route53.ResourceRecordSet{ + &route53.ResourceRecordSet{ + Type: aws.String("A"), + Name: aws.String("public-ip.foo.com."), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String("127.0.0.1"), + }, + }, + }, + &route53.ResourceRecordSet{ + Type: aws.String("TXT"), + Name: aws.String("public-ip.foo.com."), + }, &route53.ResourceRecordSet{ Type: aws.String("A"), Name: aws.String("update.foo.com."), @@ -743,6 +877,21 @@ func TestAWSConsumer(t *testing.T) { //exclude IP endpoints for now only Alias w Name: aws.String("update.example.com."), }, }, + "foo.com.": []*route53.ResourceRecordSet{ + &route53.ResourceRecordSet{ + Type: aws.String("A"), + Name: aws.String("public-ip.foo.com."), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String("127.0.0.1"), + }, + }, + }, + &route53.ResourceRecordSet{ + Type: aws.String("TXT"), + Name: aws.String("public-ip.foo.com."), + }, + }, }, }, { msg: "process new", @@ -759,7 +908,27 @@ func TestAWSConsumer(t *testing.T) { //exclude IP endpoints for now only Alias w }, &route53.ResourceRecordSet{ Type: aws.String("TXT"), - Name: aws.String("baz.org."), + Name: aws.String("process.example.com."), + }, + }, + }, + }, { + msg: "process new ip", + process: &pkg.Endpoint{DNSName: "process.example.com.", IP: "127.0.0.2"}, + expectCreate: map[string][]*route53.ResourceRecordSet{ + "example.com.": []*route53.ResourceRecordSet{ + &route53.ResourceRecordSet{ + Type: aws.String("A"), + Name: aws.String("process.example.com."), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String("127.0.0.2"), + }, + }, + }, + &route53.ResourceRecordSet{ + Type: aws.String("TXT"), + Name: aws.String("process.example.com."), }, }, }, diff --git a/pkg/aws/test/test_data.go b/pkg/aws/test/test_data.go index fda243f..d9de49f 100644 --- a/pkg/aws/test/test_data.go +++ b/pkg/aws/test/test_data.go @@ -16,6 +16,24 @@ func GetHostedZones() map[string]string { func GetOriginalState(groupID string) map[string][]*route53.ResourceRecordSet { return map[string][]*route53.ResourceRecordSet{ "foo.com.": []*route53.ResourceRecordSet{ + &route53.ResourceRecordSet{ + Type: aws.String("A"), + Name: aws.String("public-ip.foo.com."), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String("127.0.0.1"), + }, + }, + }, + &route53.ResourceRecordSet{ + Type: aws.String("TXT"), + Name: aws.String("public-ip.foo.com."), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String(groupID), + }, + }, + }, &route53.ResourceRecordSet{ Type: aws.String("A"), Name: aws.String("test.foo.com."), @@ -43,6 +61,15 @@ func GetOriginalState(groupID string) map[string][]*route53.ResourceRecordSet { }, }, "example.com.": []*route53.ResourceRecordSet{ + &route53.ResourceRecordSet{ + Type: aws.String("A"), + Name: aws.String("public-ip.example.com."), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{ + Value: aws.String("192.168.0.1"), + }, + }, + }, &route53.ResourceRecordSet{ Type: aws.String("A"), Name: aws.String("test.example.com."),