mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-30 17:53:21 -04:00 
			
		
		
		
	plugin/route53: add split zone support (#2160)
Automatically submitted.
This commit is contained in:
		| @@ -21,7 +21,9 @@ route53 [ZONE:HOSTED_ZONE_ID...] { | |||||||
| } | } | ||||||
| ~~~ | ~~~ | ||||||
|  |  | ||||||
| * **ZONE** the name of the domain to be accessed. | * **ZONE** the name of the domain to be accessed. When there are multiple zones with overlapping domains | ||||||
|  |   (private vs. public hosted zone), CoreDNS does the lookup in the given order here. Therefore, for a | ||||||
|  |   non-existing resource record, SOA response will be from the rightmost zone. | ||||||
| * **HOSTED_ZONE_ID** the ID of the hosted zone that contains the resource record sets to be accessed. | * **HOSTED_ZONE_ID** the ID of the hosted zone that contains the resource record sets to be accessed. | ||||||
| * **AWS_ACCESS_KEY_ID** and **AWS_SECRET_ACCESS_KEY** the AWS access key ID and secret access key | * **AWS_ACCESS_KEY_ID** and **AWS_SECRET_ACCESS_KEY** the AWS access key ID and secret access key | ||||||
|    to be used when query AWS (optional).  If they are not provided, then coredns tries to access |    to be used when query AWS (optional).  If they are not provided, then coredns tries to access | ||||||
| @@ -81,3 +83,11 @@ Enable route53 with AWS credentials file: | |||||||
|     } |     } | ||||||
| } | } | ||||||
| ~~~ | ~~~ | ||||||
|  |  | ||||||
|  | Enable route53 with multiple hosted zones with the same domain: | ||||||
|  |  | ||||||
|  | ~~~ txt | ||||||
|  | . { | ||||||
|  |     route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7 example.org.:Z93A52145678156 | ||||||
|  | } | ||||||
|  | ~~~ | ||||||
|   | |||||||
| @@ -30,28 +30,39 @@ type Route53 struct { | |||||||
| 	upstream  *upstream.Upstream | 	upstream  *upstream.Upstream | ||||||
|  |  | ||||||
| 	zMu   sync.RWMutex | 	zMu   sync.RWMutex | ||||||
| 	zones map[string]*zone | 	zones zones | ||||||
| } | } | ||||||
|  |  | ||||||
| type zone struct { | type zone struct { | ||||||
| 	id string | 	id  string | ||||||
| 	z  *file.Zone | 	z   *file.Zone | ||||||
|  | 	dns string | ||||||
| } | } | ||||||
|  |  | ||||||
| // New returns new *Route53. | type zones map[string][]*zone | ||||||
| func New(ctx context.Context, c route53iface.Route53API, keys map[string]string, up *upstream.Upstream) (*Route53, error) { |  | ||||||
| 	zones := make(map[string]*zone, len(keys)) | // New reads from the keys map which uses domain names as its key and hosted | ||||||
|  | // zone id lists as its values, validates that each domain name/zone id pair does | ||||||
|  | // exist, and returns a new *Route53. In addition to this, upstream is passed | ||||||
|  | // for doing recursive queries against CNAMEs. | ||||||
|  | // Returns error if it cannot verify any given domain name/zone id pair. | ||||||
|  | func New(ctx context.Context, c route53iface.Route53API, keys map[string][]string, up *upstream.Upstream) (*Route53, error) { | ||||||
|  | 	zones := make(map[string][]*zone, len(keys)) | ||||||
| 	zoneNames := make([]string, 0, len(keys)) | 	zoneNames := make([]string, 0, len(keys)) | ||||||
| 	for dns, id := range keys { | 	for dns, hostedZoneIDs := range keys { | ||||||
| 		_, err := c.ListHostedZonesByNameWithContext(ctx, &route53.ListHostedZonesByNameInput{ | 		for _, hostedZoneID := range hostedZoneIDs { | ||||||
| 			DNSName:      aws.String(dns), | 			_, err := c.ListHostedZonesByNameWithContext(ctx, &route53.ListHostedZonesByNameInput{ | ||||||
| 			HostedZoneId: aws.String(id), | 				DNSName:      aws.String(dns), | ||||||
| 		}) | 				HostedZoneId: aws.String(hostedZoneID), | ||||||
| 		if err != nil { | 			}) | ||||||
| 			return nil, err | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			if _, ok := zones[dns]; !ok { | ||||||
|  | 				zoneNames = append(zoneNames, dns) | ||||||
|  | 			} | ||||||
|  | 			zones[dns] = append(zones[dns], &zone{id: hostedZoneID, dns: dns, z: file.NewZone(dns, "")}) | ||||||
| 		} | 		} | ||||||
| 		zones[dns] = &zone{id: id, z: file.NewZone(dns, "")} |  | ||||||
| 		zoneNames = append(zoneNames, dns) |  | ||||||
| 	} | 	} | ||||||
| 	return &Route53{ | 	return &Route53{ | ||||||
| 		client:    c, | 		client:    c, | ||||||
| @@ -101,9 +112,14 @@ func (h *Route53) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg | |||||||
| 	m.SetReply(r) | 	m.SetReply(r) | ||||||
| 	m.Authoritative, m.RecursionAvailable = true, true | 	m.Authoritative, m.RecursionAvailable = true, true | ||||||
| 	var result file.Result | 	var result file.Result | ||||||
| 	h.zMu.RLock() | 	for _, hostedZone := range z { | ||||||
| 	m.Answer, m.Ns, m.Extra, result = z.z.Lookup(state, qname) | 		h.zMu.RLock() | ||||||
| 	h.zMu.RUnlock() | 		m.Answer, m.Ns, m.Extra, result = hostedZone.z.Lookup(state, qname) | ||||||
|  | 		h.zMu.RUnlock() | ||||||
|  | 		if len(m.Answer) != 0 { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if len(m.Answer) == 0 && h.Fall.Through(qname) { | 	if len(m.Answer) == 0 && h.Fall.Through(qname) { | ||||||
| 		return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) | 		return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) | ||||||
| @@ -146,36 +162,37 @@ func (h *Route53) updateZones(ctx context.Context) error { | |||||||
| 	errc := make(chan error) | 	errc := make(chan error) | ||||||
| 	defer close(errc) | 	defer close(errc) | ||||||
| 	for zName, z := range h.zones { | 	for zName, z := range h.zones { | ||||||
| 		go func(zName string, z *zone) { | 		go func(zName string, z []*zone) { | ||||||
| 			var err error | 			var err error | ||||||
| 			defer func() { | 			defer func() { | ||||||
| 				errc <- err | 				errc <- err | ||||||
| 			}() | 			}() | ||||||
|  |  | ||||||
| 			newZ := file.NewZone(zName, "") | 			for i, hostedZone := range z { | ||||||
| 			newZ.Upstream = *h.upstream | 				newZ := file.NewZone(zName, "") | ||||||
|  | 				newZ.Upstream = *h.upstream | ||||||
| 			in := &route53.ListResourceRecordSetsInput{ | 				in := &route53.ListResourceRecordSetsInput{ | ||||||
| 				HostedZoneId: aws.String(z.id), | 					HostedZoneId: aws.String(hostedZone.id), | ||||||
| 			} | 				} | ||||||
| 			err = h.client.ListResourceRecordSetsPagesWithContext(ctx, in, | 				err = h.client.ListResourceRecordSetsPagesWithContext(ctx, in, | ||||||
| 				func(out *route53.ListResourceRecordSetsOutput, last bool) bool { | 					func(out *route53.ListResourceRecordSetsOutput, last bool) bool { | ||||||
| 					for _, rrs := range out.ResourceRecordSets { | 						for _, rrs := range out.ResourceRecordSets { | ||||||
| 						if err := updateZoneFromRRS(rrs, newZ); err != nil { | 							if err := updateZoneFromRRS(rrs, newZ); err != nil { | ||||||
| 							// Maybe unsupported record type. Log and carry on. | 								// Maybe unsupported record type. Log and carry on. | ||||||
| 							log.Warningf("Failed to process resource record set: %v", err) | 								log.Warningf("Failed to process resource record set: %v", err) | ||||||
|  | 							} | ||||||
| 						} | 						} | ||||||
| 					} | 						return true | ||||||
| 					return true | 					}) | ||||||
| 				}) | 				if err != nil { | ||||||
| 			if err != nil { | 					err = fmt.Errorf("failed to list resource records for %v:%v from route53: %v", zName, hostedZone.id, err) | ||||||
| 				err = fmt.Errorf("failed to list resource records for %v:%v from route53: %v", zName, z.id, err) | 					return | ||||||
| 				return | 				} | ||||||
|  | 				h.zMu.Lock() | ||||||
|  | 				(*z[i]).z = newZ | ||||||
|  | 				h.zMu.Unlock() | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			h.zMu.Lock() |  | ||||||
| 			z.z = newZ |  | ||||||
| 			h.zMu.Unlock() |  | ||||||
| 		}(zName, z) | 		}(zName, z) | ||||||
| 	} | 	} | ||||||
| 	// Collect errors (if any). This will also sync on all zones updates | 	// Collect errors (if any). This will also sync on all zones updates | ||||||
|   | |||||||
| @@ -31,19 +31,26 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou | |||||||
| 	if aws.StringValue(in.HostedZoneId) == "0987654321" { | 	if aws.StringValue(in.HostedZoneId) == "0987654321" { | ||||||
| 		return errors.New("bad. zone is bad") | 		return errors.New("bad. zone is bad") | ||||||
| 	} | 	} | ||||||
| 	var rrs []*route53.ResourceRecordSet | 	rrsResponse := map[string][]*route53.ResourceRecordSet{} | ||||||
| 	for _, r := range []struct { | 	for _, r := range []struct { | ||||||
| 		rType, name, value string | 		rType, name, value, hostedZoneID string | ||||||
| 	}{ | 	}{ | ||||||
| 		{"A", "example.org.", "1.2.3.4"}, | 		{"A", "example.org.", "1.2.3.4", "1234567890"}, | ||||||
| 		{"AAAA", "example.org.", "2001:db8:85a3::8a2e:370:7334"}, | 		{"AAAA", "example.org.", "2001:db8:85a3::8a2e:370:7334", "1234567890"}, | ||||||
| 		{"CNAME", "sample.example.org.", "example.org"}, | 		{"CNAME", "sample.example.org.", "example.org", "1234567890"}, | ||||||
| 		{"PTR", "example.org.", "ptr.example.org."}, | 		{"PTR", "example.org.", "ptr.example.org.", "1234567890"}, | ||||||
| 		{"SOA", "org.", "ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"}, | 		{"SOA", "org.", "ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400", "1234567890"}, | ||||||
| 		{"NS", "com.", "ns-1536.awsdns-00.co.uk."}, | 		{"NS", "com.", "ns-1536.awsdns-00.co.uk.", "1234567890"}, | ||||||
| 		// Unsupported type should be ignored. | 		// Unsupported type should be ignored. | ||||||
| 		{"YOLO", "swag.", "foobar"}, | 		{"YOLO", "swag.", "foobar", "1234567890"}, | ||||||
|  | 		// hosted zone with the same name, but a different id | ||||||
|  | 		{"A", "other-example.org.", "3.5.7.9", "1357986420"}, | ||||||
|  | 		{"SOA", "org.", "ns-15.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400", "1357986420"}, | ||||||
| 	} { | 	} { | ||||||
|  | 		rrs, ok := rrsResponse[r.hostedZoneID] | ||||||
|  | 		if !ok { | ||||||
|  | 			rrs = make([]*route53.ResourceRecordSet, 0) | ||||||
|  | 		} | ||||||
| 		rrs = append(rrs, &route53.ResourceRecordSet{Type: aws.String(r.rType), | 		rrs = append(rrs, &route53.ResourceRecordSet{Type: aws.String(r.rType), | ||||||
| 			Name: aws.String(r.name), | 			Name: aws.String(r.name), | ||||||
| 			ResourceRecords: []*route53.ResourceRecord{ | 			ResourceRecords: []*route53.ResourceRecord{ | ||||||
| @@ -53,9 +60,11 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou | |||||||
| 			}, | 			}, | ||||||
| 			TTL: aws.Int64(300), | 			TTL: aws.Int64(300), | ||||||
| 		}) | 		}) | ||||||
|  | 		rrsResponse[r.hostedZoneID] = rrs | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if ok := fn(&route53.ListResourceRecordSetsOutput{ | 	if ok := fn(&route53.ListResourceRecordSetsOutput{ | ||||||
| 		ResourceRecordSets: rrs, | 		ResourceRecordSets: rrsResponse[aws.StringValue(in.HostedZoneId)], | ||||||
| 	}, true); !ok { | 	}, true); !ok { | ||||||
| 		return errors.New("paging function return false") | 		return errors.New("paging function return false") | ||||||
| 	} | 	} | ||||||
| @@ -65,7 +74,7 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou | |||||||
| func TestRoute53(t *testing.T) { | func TestRoute53(t *testing.T) { | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
|  |  | ||||||
| 	r, err := New(ctx, fakeRoute53{}, map[string]string{"bad.": "0987654321"}, &upstream.Upstream{}) | 	r, err := New(ctx, fakeRoute53{}, map[string][]string{"bad.": []string{"0987654321"}}, &upstream.Upstream{}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Failed to create Route53: %v", err) | 		t.Fatalf("Failed to create Route53: %v", err) | ||||||
| 	} | 	} | ||||||
| @@ -73,7 +82,7 @@ func TestRoute53(t *testing.T) { | |||||||
| 		t.Fatalf("Expected errors for zone bad.") | 		t.Fatalf("Expected errors for zone bad.") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	r, err = New(ctx, fakeRoute53{}, map[string]string{"org.": "1234567890", "gov.": "Z098765432"}, &upstream.Upstream{}) | 	r, err = New(ctx, fakeRoute53{}, map[string][]string{"org.": []string{"1357986420", "1234567890"}, "gov": []string{"Z098765432"}}, &upstream.Upstream{}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Failed to create Route53: %v", err) | 		t.Fatalf("Failed to create Route53: %v", err) | ||||||
| 	} | 	} | ||||||
| @@ -158,7 +167,7 @@ func TestRoute53(t *testing.T) { | |||||||
| 			qname:        "example.org", | 			qname:        "example.org", | ||||||
| 			qtype:        dns.TypeSOA, | 			qtype:        dns.TypeSOA, | ||||||
| 			expectedCode: dns.RcodeSuccess, | 			expectedCode: dns.RcodeSuccess, | ||||||
| 			wantAnswer: []string{"org.	300	IN	SOA	ns-1536.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"}, | 			wantAnswer: []string{"org.	300	IN	SOA	ns-15.awsdns-00.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"}, | ||||||
| 		}, | 		}, | ||||||
| 		// 6. Explicit SOA query for example.org. | 		// 6. Explicit SOA query for example.org. | ||||||
| 		{ | 		{ | ||||||
| @@ -187,6 +196,13 @@ func TestRoute53(t *testing.T) { | |||||||
| 			expectedCode: dns.RcodeSuccess, | 			expectedCode: dns.RcodeSuccess, | ||||||
| 			wantAnswer: []string{"example.gov.	300	IN	A	2.4.6.8"}, | 			wantAnswer: []string{"example.gov.	300	IN	A	2.4.6.8"}, | ||||||
| 		}, | 		}, | ||||||
|  | 		// 10. other-zone.example.org is stored in a different hosted zone. success | ||||||
|  | 		{ | ||||||
|  | 			qname:        "other-example.org", | ||||||
|  | 			qtype:        dns.TypeA, | ||||||
|  | 			expectedCode: dns.RcodeSuccess, | ||||||
|  | 			wantAnswer: []string{"other-example.org.	300	IN	A	3.5.7.9"}, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for ti, tc := range tests { | 	for ti, tc := range tests { | ||||||
|   | |||||||
| @@ -35,7 +35,8 @@ func init() { | |||||||
| } | } | ||||||
|  |  | ||||||
| func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Route53API) error { | func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Route53API) error { | ||||||
| 	keys := map[string]string{} | 	keyPairs := map[string]struct{}{} | ||||||
|  | 	keys := map[string][]string{} | ||||||
|  |  | ||||||
| 	// Route53 plugin attempts to find AWS credentials by using ChainCredentials. | 	// Route53 plugin attempts to find AWS credentials by using ChainCredentials. | ||||||
| 	// And the order of that provider chain is as follows: | 	// And the order of that provider chain is as follows: | ||||||
| @@ -56,14 +57,16 @@ func setup(c *caddy.Controller, f func(*credentials.Credentials) route53iface.Ro | |||||||
| 			if len(parts) != 2 { | 			if len(parts) != 2 { | ||||||
| 				return c.Errf("invalid zone '%s'", args[i]) | 				return c.Errf("invalid zone '%s'", args[i]) | ||||||
| 			} | 			} | ||||||
| 			if parts[0] == "" || parts[1] == "" { | 			dns, hostedZoneID := parts[0], parts[1] | ||||||
|  | 			if dns == "" || hostedZoneID == "" { | ||||||
| 				return c.Errf("invalid zone '%s'", args[i]) | 				return c.Errf("invalid zone '%s'", args[i]) | ||||||
| 			} | 			} | ||||||
| 			zone := plugin.Host(parts[0]).Normalize() | 			if _, ok := keyPairs[args[i]]; ok { | ||||||
| 			if v, ok := keys[zone]; ok && v != parts[1] { | 				return c.Errf("conflict zone '%s'", args[i]) | ||||||
| 				return c.Errf("conflict zone '%s' ('%s' vs. '%s')", zone, v, parts[1]) |  | ||||||
| 			} | 			} | ||||||
| 			keys[zone] = parts[1] |  | ||||||
|  | 			keyPairs[args[i]] = struct{}{} | ||||||
|  | 			keys[dns] = append(keys[dns], hostedZoneID) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		for c.NextBlock() { | 		for c.NextBlock() { | ||||||
|   | |||||||
| @@ -53,6 +53,10 @@ func TestSetupRoute53(t *testing.T) { | |||||||
|     aws_access_key ACCESS_KEY_ID SEKRIT_ACCESS_KEY |     aws_access_key ACCESS_KEY_ID SEKRIT_ACCESS_KEY | ||||||
|     upstream 1.2.3.4 |     upstream 1.2.3.4 | ||||||
| }`) | }`) | ||||||
|  | 	if err := setup(c, f); err != nil { | ||||||
|  | 		t.Fatalf("Unexpected errors: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	c = caddy.NewTestController("dns", `route53 example.org:12345678 { | 	c = caddy.NewTestController("dns", `route53 example.org:12345678 { | ||||||
|     fallthrough |     fallthrough | ||||||
| }`) | }`) | ||||||
| @@ -91,4 +95,17 @@ func TestSetupRoute53(t *testing.T) { | |||||||
| 	if err := setup(c, f); err == nil { | 	if err := setup(c, f); err == nil { | ||||||
| 		t.Fatalf("Expected errors, but got: %v", err) | 		t.Fatalf("Expected errors, but got: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	c = caddy.NewTestController("dns", `route53 example.org:12345678 example.org:12345678 { | ||||||
|  |  		upstream 1.2.3.4 | ||||||
|  | 	}`) | ||||||
|  | 	if err := setup(c, f); err == nil { | ||||||
|  | 		t.Fatalf("Expected errors, but got: %v", err) | ||||||
|  | 	} | ||||||
|  | 	c = caddy.NewTestController("dns", `route53 example.org { | ||||||
|  |  		upstream 1.2.3.4 | ||||||
|  | 	}`) | ||||||
|  | 	if err := setup(c, f); err == nil { | ||||||
|  | 		t.Fatalf("Expected errors, but got: %v", err) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user