external-dns/provider/awssd/aws_sd_test.go
Guðlaugur Stefán Egilsson e64e5368ee
fix(aws-sd): service instances registration and deregistration (#5135)
* Only de-register removed targets

* Use maps for current targets lookup.

* Use camelCase, not _
2025-03-05 00:23:46 -08:00

1068 lines
32 KiB
Go

/*
Copyright 2018 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 awssd
import (
"context"
"errors"
"math/rand"
"reflect"
"strconv"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
sd "github.com/aws/aws-sdk-go-v2/service/servicediscovery"
sdtypes "github.com/aws/aws-sdk-go-v2/service/servicediscovery/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan"
)
// Compile time check for interface conformance
var _ AWSSDClient = &AWSSDClientStub{}
var (
ErrNamespaceNotFound = errors.New("Namespace not found")
)
type AWSSDClientStub struct {
// map[namespace_id]namespace
namespaces map[string]*sdtypes.Namespace
// map[namespace_id] => map[service_id]instance
services map[string]map[string]*sdtypes.Service
// map[service_id] => map[inst_id]instance
instances map[string]map[string]*sdtypes.Instance
// []inst_id
deregistered []string
}
func (s *AWSSDClientStub) CreateService(ctx context.Context, input *sd.CreateServiceInput, optFns ...func(*sd.Options)) (*sd.CreateServiceOutput, error) {
srv := &sdtypes.Service{
Id: aws.String(strconv.Itoa(rand.Intn(10000))),
DnsConfig: input.DnsConfig,
Name: input.Name,
Description: input.Description,
CreateDate: aws.Time(time.Now()),
CreatorRequestId: input.CreatorRequestId,
}
nsServices, ok := s.services[*input.NamespaceId]
if !ok {
nsServices = make(map[string]*sdtypes.Service)
s.services[*input.NamespaceId] = nsServices
}
nsServices[*srv.Id] = srv
return &sd.CreateServiceOutput{
Service: srv,
}, nil
}
func (s *AWSSDClientStub) DeregisterInstance(ctx context.Context, input *sd.DeregisterInstanceInput, optFns ...func(options *sd.Options)) (*sd.DeregisterInstanceOutput, error) {
serviceInstances := s.instances[*input.ServiceId]
delete(serviceInstances, *input.InstanceId)
s.deregistered = append(s.deregistered, *input.InstanceId)
return &sd.DeregisterInstanceOutput{}, nil
}
func (s *AWSSDClientStub) GetService(ctx context.Context, input *sd.GetServiceInput, optFns ...func(options *sd.Options)) (*sd.GetServiceOutput, error) {
for _, entry := range s.services {
srv, ok := entry[*input.Id]
if ok {
return &sd.GetServiceOutput{
Service: srv,
}, nil
}
}
return nil, errors.New("service not found")
}
func (s *AWSSDClientStub) DiscoverInstances(ctx context.Context, input *sd.DiscoverInstancesInput, opts ...func(options *sd.Options)) (*sd.DiscoverInstancesOutput, error) {
instances := make([]sdtypes.HttpInstanceSummary, 0)
var foundNs bool
for _, ns := range s.namespaces {
if *ns.Name == *input.NamespaceName {
foundNs = true
for _, srv := range s.services[*ns.Id] {
if *srv.Name == *input.ServiceName {
for _, inst := range s.instances[*srv.Id] {
instances = append(instances, *instanceToHTTPInstanceSummary(inst))
}
}
}
}
}
if !foundNs {
return nil, ErrNamespaceNotFound
}
return &sd.DiscoverInstancesOutput{
Instances: instances,
}, nil
}
func (s *AWSSDClientStub) ListNamespaces(ctx context.Context, input *sd.ListNamespacesInput, optFns ...func(options *sd.Options)) (*sd.ListNamespacesOutput, error) {
namespaces := make([]sdtypes.NamespaceSummary, 0)
for _, ns := range s.namespaces {
if len(input.Filters) > 0 && input.Filters[0].Name == sdtypes.NamespaceFilterNameType {
if ns.Type != sdtypes.NamespaceType(input.Filters[0].Values[0]) {
// skip namespaces not matching filter
continue
}
}
namespaces = append(namespaces, *namespaceToNamespaceSummary(ns))
}
return &sd.ListNamespacesOutput{
Namespaces: namespaces,
}, nil
}
func (s *AWSSDClientStub) ListServices(ctx context.Context, input *sd.ListServicesInput, optFns ...func(options *sd.Options)) (*sd.ListServicesOutput, error) {
services := make([]sdtypes.ServiceSummary, 0)
// get namespace filter
if len(input.Filters) == 0 || input.Filters[0].Name != sdtypes.ServiceFilterNameNamespaceId {
return nil, errors.New("missing namespace filter")
}
nsID := input.Filters[0].Values[0]
for _, srv := range s.services[nsID] {
services = append(services, *serviceToServiceSummary(srv))
}
return &sd.ListServicesOutput{
Services: services,
}, nil
}
func (s *AWSSDClientStub) RegisterInstance(ctx context.Context, input *sd.RegisterInstanceInput, optFns ...func(options *sd.Options)) (*sd.RegisterInstanceOutput, error) {
srvInstances, ok := s.instances[*input.ServiceId]
if !ok {
srvInstances = make(map[string]*sdtypes.Instance)
s.instances[*input.ServiceId] = srvInstances
}
srvInstances[*input.InstanceId] = &sdtypes.Instance{
Id: input.InstanceId,
Attributes: input.Attributes,
CreatorRequestId: input.CreatorRequestId,
}
return &sd.RegisterInstanceOutput{}, nil
}
func (s *AWSSDClientStub) UpdateService(ctx context.Context, input *sd.UpdateServiceInput, optFns ...func(options *sd.Options)) (*sd.UpdateServiceOutput, error) {
out, err := s.GetService(ctx, &sd.GetServiceInput{Id: input.Id})
if err != nil {
return nil, err
}
origSrv := out.Service
updateSrv := input.Service
origSrv.Description = updateSrv.Description
origSrv.DnsConfig.DnsRecords = updateSrv.DnsConfig.DnsRecords
return &sd.UpdateServiceOutput{}, nil
}
func (s *AWSSDClientStub) DeleteService(ctx context.Context, input *sd.DeleteServiceInput, optFns ...func(options *sd.Options)) (*sd.DeleteServiceOutput, error) {
out, err := s.GetService(ctx, &sd.GetServiceInput{Id: input.Id})
if err != nil {
return nil, err
}
service := out.Service
namespace := s.services[*service.NamespaceId]
delete(namespace, *input.Id)
return &sd.DeleteServiceOutput{}, nil
}
func newTestAWSSDProvider(api AWSSDClient, domainFilter endpoint.DomainFilter, namespaceTypeFilter, ownerID string) *AWSSDProvider {
return &AWSSDProvider{
client: api,
dryRun: false,
namespaceFilter: domainFilter,
namespaceTypeFilter: newSdNamespaceFilter(namespaceTypeFilter),
cleanEmptyService: true,
ownerID: ownerID,
}
}
func instanceToHTTPInstanceSummary(instance *sdtypes.Instance) *sdtypes.HttpInstanceSummary {
if instance == nil {
return nil
}
return &sdtypes.HttpInstanceSummary{
InstanceId: instance.Id,
Attributes: instance.Attributes,
}
}
func namespaceToNamespaceSummary(namespace *sdtypes.Namespace) *sdtypes.NamespaceSummary {
if namespace == nil {
return nil
}
return &sdtypes.NamespaceSummary{
Id: namespace.Id,
Type: namespace.Type,
Name: namespace.Name,
Arn: namespace.Arn,
}
}
func serviceToServiceSummary(service *sdtypes.Service) *sdtypes.ServiceSummary {
if service == nil {
return nil
}
return &sdtypes.ServiceSummary{
Arn: service.Arn,
CreateDate: service.CreateDate,
Description: service.Description,
DnsConfig: service.DnsConfig,
HealthCheckConfig: service.HealthCheckConfig,
HealthCheckCustomConfig: service.HealthCheckCustomConfig,
Id: service.Id,
InstanceCount: service.InstanceCount,
Name: service.Name,
Type: service.Type,
}
}
func TestAWSSDProvider_Records(t *testing.T) {
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
services := map[string]map[string]*sdtypes.Service{
"private": {
"a-srv": {
Id: aws.String("a-srv"),
Name: aws.String("service1"),
NamespaceId: aws.String("private"),
Description: aws.String("owner-id"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(100),
}},
},
},
"alias-srv": {
Id: aws.String("alias-srv"),
Name: aws.String("service2"),
NamespaceId: aws.String("private"),
Description: aws.String("owner-id"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(100),
}},
},
},
"cname-srv": {
Id: aws.String("cname-srv"),
Name: aws.String("service3"),
NamespaceId: aws.String("private"),
Description: aws.String("owner-id"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeCname,
TTL: aws.Int64(80),
}},
},
},
"aaaa-srv": {
Id: aws.String("aaaa-srv"),
Name: aws.String("service4"),
Description: aws.String("owner-id"),
DnsConfig: &sdtypes.DnsConfig{
NamespaceId: aws.String("private"),
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeAaaa,
TTL: aws.Int64(100),
}},
},
},
},
}
instances := map[string]map[string]*sdtypes.Instance{
"a-srv": {
"1.2.3.4": {
Id: aws.String("1.2.3.4"),
Attributes: map[string]string{
sdInstanceAttrIPV4: "1.2.3.4",
},
},
"1.2.3.5": {
Id: aws.String("1.2.3.5"),
Attributes: map[string]string{
sdInstanceAttrIPV4: "1.2.3.5",
},
},
},
"alias-srv": {
"load-balancer.us-east-1.elb.amazonaws.com": {
Id: aws.String("load-balancer.us-east-1.elb.amazonaws.com"),
Attributes: map[string]string{
sdInstanceAttrAlias: "load-balancer.us-east-1.elb.amazonaws.com",
},
},
},
"cname-srv": {
"cname.target.com": {
Id: aws.String("cname.target.com"),
Attributes: map[string]string{
sdInstanceAttrCname: "cname.target.com",
},
},
},
"aaaa-srv": {
"0000:0000:0000:0000:abcd:abcd:abcd:abcd": {
Id: aws.String("0000:0000:0000:0000:abcd:abcd:abcd:abcd"),
Attributes: map[string]string{
sdInstanceAttrIPV6: "0000:0000:0000:0000:abcd:abcd:abcd:abcd",
},
},
},
}
expectedEndpoints := []*endpoint.Endpoint{
{DNSName: "service1.private.com", Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5"}, RecordType: endpoint.RecordTypeA, RecordTTL: 100, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}},
{DNSName: "service2.private.com", Targets: endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 100, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}},
{DNSName: "service3.private.com", Targets: endpoint.Targets{"cname.target.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 80, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}},
{DNSName: "service4.private.com", Targets: endpoint.Targets{"0000:0000:0000:0000:abcd:abcd:abcd:abcd"}, RecordType: endpoint.RecordTypeAAAA, RecordTTL: 100, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: services,
instances: instances,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
endpoints, _ := provider.Records(context.Background())
assert.True(t, testutils.SameEndpoints(expectedEndpoints, endpoints), "expected and actual endpoints don't match, expected=%v, actual=%v", expectedEndpoints, endpoints)
}
func TestAWSSDProvider_ApplyChanges(t *testing.T) {
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: make(map[string]map[string]*sdtypes.Service),
instances: make(map[string]map[string]*sdtypes.Instance),
}
expectedEndpoints := []*endpoint.Endpoint{
{DNSName: "service1.private.com", Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5"}, RecordType: endpoint.RecordTypeA, RecordTTL: 60},
{DNSName: "service2.private.com", Targets: endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 80},
{DNSName: "service3.private.com", Targets: endpoint.Targets{"cname.target.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 100},
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
ctx := context.Background()
// apply creates
provider.ApplyChanges(ctx, &plan.Changes{
Create: expectedEndpoints,
})
// make sure services were created
assert.Len(t, api.services["private"], 3)
existingServices, _ := provider.ListServicesByNamespaceID(context.Background(), namespaces["private"].Id)
assert.NotNil(t, existingServices["service1"])
assert.NotNil(t, existingServices["service2"])
assert.NotNil(t, existingServices["service3"])
// make sure instances were registered
endpoints, _ := provider.Records(ctx)
assert.True(t, testutils.SameEndpoints(expectedEndpoints, endpoints), "expected and actual endpoints don't match, expected=%v, actual=%v", expectedEndpoints, endpoints)
ctx = context.Background()
// apply deletes
provider.ApplyChanges(ctx, &plan.Changes{
Delete: expectedEndpoints,
})
// make sure all instances are gone
endpoints, _ = provider.Records(ctx)
assert.Empty(t, endpoints)
}
func TestAWSSDProvider_ApplyChanges_Update(t *testing.T) {
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: make(map[string]map[string]*sdtypes.Service),
instances: make(map[string]map[string]*sdtypes.Instance),
}
oldEndpoints := []*endpoint.Endpoint{
{DNSName: "service1.private.com", Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5"}, RecordType: endpoint.RecordTypeA, RecordTTL: 60},
}
newEndpoints := []*endpoint.Endpoint{
{DNSName: "service1.private.com", Targets: endpoint.Targets{"1.2.3.4", "1.2.3.6"}, RecordType: endpoint.RecordTypeA, RecordTTL: 60},
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
ctx := context.Background()
// apply creates
provider.ApplyChanges(ctx, &plan.Changes{
Create: oldEndpoints,
})
ctx = context.Background()
// apply update
provider.ApplyChanges(ctx, &plan.Changes{
UpdateOld: oldEndpoints,
UpdateNew: newEndpoints,
})
// make sure services were created
assert.Len(t, api.services["private"], 1)
existingServices, _ := provider.ListServicesByNamespaceID(ctx, namespaces["private"].Id)
assert.NotNil(t, existingServices["service1"])
// make sure instances were registered
endpoints, _ := provider.Records(ctx)
assert.True(t, testutils.SameEndpoints(newEndpoints, endpoints), "expected and actual endpoints don't match, expected=%v, actual=%v", newEndpoints, endpoints)
// make sure only one instance is de-registered
assert.Len(t, api.deregistered, 1)
assert.Equal(t, api.deregistered[0], "1.2.3.5", "wrong target de-registered")
}
func TestAWSSDProvider_ListNamespaces(t *testing.T) {
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
"public": {
Id: aws.String("public"),
Name: aws.String("public.com"),
Type: sdtypes.NamespaceTypeDnsPublic,
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
}
for _, tc := range []struct {
msg string
domainFilter endpoint.DomainFilter
namespaceTypeFilter string
expectedNamespaces []*sdtypes.NamespaceSummary
}{
{"public filter", endpoint.NewDomainFilter([]string{}), "public", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
{"private filter", endpoint.NewDomainFilter([]string{}), "private", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["private"])}},
{"domain filter", endpoint.NewDomainFilter([]string{"public.com"}), "", []*sdtypes.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
{"non-existing domain", endpoint.NewDomainFilter([]string{"xxx.com"}), "", []*sdtypes.NamespaceSummary{}},
} {
provider := newTestAWSSDProvider(api, tc.domainFilter, tc.namespaceTypeFilter, "")
result, err := provider.ListNamespaces(context.Background())
require.NoError(t, err)
expectedMap := make(map[string]*sdtypes.NamespaceSummary)
resultMap := make(map[string]*sdtypes.NamespaceSummary)
for _, ns := range tc.expectedNamespaces {
expectedMap[*ns.Id] = ns
}
for _, ns := range result {
resultMap[*ns.Id] = ns
}
if !reflect.DeepEqual(resultMap, expectedMap) {
t.Errorf("AWSSDProvider.ListNamespaces() error = %v, wantErr %v", result, tc.expectedNamespaces)
}
}
}
func TestAWSSDProvider_ListServicesByNamespace(t *testing.T) {
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
"public": {
Id: aws.String("public"),
Name: aws.String("public.com"),
Type: sdtypes.NamespaceTypeDnsPublic,
},
}
services := map[string]map[string]*sdtypes.Service{
"private": {
"srv1": {
Id: aws.String("srv1"),
Name: aws.String("service1"),
NamespaceId: aws.String("private"),
},
"srv2": {
Id: aws.String("srv2"),
Name: aws.String("service2"),
NamespaceId: aws.String("private"),
},
},
"public": {
"srv3": {
Id: aws.String("srv3"),
Name: aws.String("service3"),
NamespaceId: aws.String("public"),
},
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: services,
}
for _, tc := range []struct {
expectedServices map[string]*sdtypes.Service
}{
{map[string]*sdtypes.Service{"service1": services["private"]["srv1"], "service2": services["private"]["srv2"]}},
} {
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
result, err := provider.ListServicesByNamespaceID(context.Background(), namespaces["private"].Id)
require.NoError(t, err)
assert.Equal(t, tc.expectedServices, result)
}
}
func TestAWSSDProvider_CreateService(t *testing.T) {
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: make(map[string]map[string]*sdtypes.Service),
}
expectedServices := make(map[string]*sdtypes.Service)
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
// A type
provider.CreateService(context.Background(), aws.String("private"), aws.String("A-srv"), &endpoint.Endpoint{
Labels: map[string]string{
endpoint.AWSSDDescriptionLabel: "A-srv",
},
RecordType: endpoint.RecordTypeA,
RecordTTL: 60,
Targets: endpoint.Targets{"1.2.3.4"},
})
expectedServices["A-srv"] = &sdtypes.Service{
Name: aws.String("A-srv"),
Description: aws.String("A-srv"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyMultivalue,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(60),
}},
},
NamespaceId: aws.String("private"),
}
// AAAA type
provider.CreateService(context.Background(), aws.String("private"), aws.String("AAAA-srv"), &endpoint.Endpoint{
Labels: map[string]string{
endpoint.AWSSDDescriptionLabel: "AAAA-srv",
},
RecordType: endpoint.RecordTypeAAAA,
RecordTTL: 60,
Targets: endpoint.Targets{"::1234:5678:"},
})
expectedServices["AAAA-srv"] = &sdtypes.Service{
Name: aws.String("AAAA-srv"),
Description: aws.String("AAAA-srv"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyMultivalue,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeAaaa,
TTL: aws.Int64(60),
}},
},
NamespaceId: aws.String("private"),
}
// CNAME type
provider.CreateService(context.Background(), aws.String("private"), aws.String("CNAME-srv"), &endpoint.Endpoint{
Labels: map[string]string{
endpoint.AWSSDDescriptionLabel: "CNAME-srv",
},
RecordType: endpoint.RecordTypeCNAME,
RecordTTL: 80,
Targets: endpoint.Targets{"cname.target.com"},
})
expectedServices["CNAME-srv"] = &sdtypes.Service{
Name: aws.String("CNAME-srv"),
Description: aws.String("CNAME-srv"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeCname,
TTL: aws.Int64(80),
}},
},
NamespaceId: aws.String("private"),
}
// ALIAS type
provider.CreateService(context.Background(), aws.String("private"), aws.String("ALIAS-srv"), &endpoint.Endpoint{
Labels: map[string]string{
endpoint.AWSSDDescriptionLabel: "ALIAS-srv",
},
RecordType: endpoint.RecordTypeCNAME,
RecordTTL: 100,
Targets: endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com"},
})
expectedServices["ALIAS-srv"] = &sdtypes.Service{
Name: aws.String("ALIAS-srv"),
Description: aws.String("ALIAS-srv"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(100),
}},
},
NamespaceId: aws.String("private"),
}
validateAWSSDServicesMapsEqual(t, expectedServices, api.services["private"])
}
func validateAWSSDServicesMapsEqual(t *testing.T, expected map[string]*sdtypes.Service, services map[string]*sdtypes.Service) {
require.Len(t, services, len(expected))
for _, srv := range services {
validateAWSSDServicesEqual(t, expected[*srv.Name], srv)
}
}
func validateAWSSDServicesEqual(t *testing.T, expected *sdtypes.Service, srv *sdtypes.Service) {
assert.Equal(t, *expected.Description, *srv.Description)
assert.Equal(t, *expected.Name, *srv.Name)
assert.True(t, reflect.DeepEqual(*expected.DnsConfig, *srv.DnsConfig))
}
func TestAWSSDProvider_UpdateService(t *testing.T) {
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
services := map[string]map[string]*sdtypes.Service{
"private": {
"srv1": {
Id: aws.String("srv1"),
Name: aws.String("service1"),
NamespaceId: aws.String("private"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyMultivalue,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(60),
}},
},
},
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: services,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
// update service with different TTL
provider.UpdateService(context.Background(), services["private"]["srv1"], &endpoint.Endpoint{
RecordType: endpoint.RecordTypeA,
RecordTTL: 100,
})
assert.Equal(t, int64(100), *api.services["private"]["srv1"].DnsConfig.DnsRecords[0].TTL)
}
func TestAWSSDProvider_DeleteService(t *testing.T) {
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
services := map[string]map[string]*sdtypes.Service{
"private": {
"srv1": {
Id: aws.String("srv1"),
Description: aws.String("heritage=external-dns,external-dns/owner=owner-id"),
Name: aws.String("service1"),
NamespaceId: aws.String("private"),
},
"srv2": {
Id: aws.String("srv2"),
Description: aws.String("heritage=external-dns,external-dns/owner=owner-id"),
Name: aws.String("service2"),
NamespaceId: aws.String("private"),
},
"srv3": {
Id: aws.String("srv3"),
Description: aws.String("heritage=external-dns,external-dns/owner=owner-id,external-dns/resource=virtualservice/grpc-server/validate-grpc-server"),
Name: aws.String("service3"),
NamespaceId: aws.String("private"),
},
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: services,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "owner-id")
// delete first service
err := provider.DeleteService(context.Background(), services["private"]["srv1"])
assert.NoError(t, err)
assert.Len(t, api.services["private"], 2)
// delete third service
err1 := provider.DeleteService(context.Background(), services["private"]["srv3"])
assert.NoError(t, err1)
assert.Len(t, api.services["private"], 1)
expectedServices := map[string]*sdtypes.Service{
"srv2": {
Id: aws.String("srv2"),
Description: aws.String("heritage=external-dns,external-dns/owner=owner-id"),
Name: aws.String("service2"),
NamespaceId: aws.String("private"),
},
}
assert.Equal(t, expectedServices, api.services["private"])
}
func TestAWSSDProvider_RegisterInstance(t *testing.T) {
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
services := map[string]map[string]*sdtypes.Service{
"private": {
"a-srv": {
Id: aws.String("a-srv"),
Name: aws.String("service1"),
NamespaceId: aws.String("private"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(60),
}},
},
},
"cname-srv": {
Id: aws.String("cname-srv"),
Name: aws.String("service2"),
NamespaceId: aws.String("private"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeCname,
TTL: aws.Int64(60),
}},
},
},
"alias-srv": {
Id: aws.String("alias-srv"),
Name: aws.String("service3"),
NamespaceId: aws.String("private"),
DnsConfig: &sdtypes.DnsConfig{
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeA,
TTL: aws.Int64(60),
}},
},
},
"aaaa-srv": {
Id: aws.String("aaaa-srv"),
Name: aws.String("service4"),
Description: aws.String("owner-id"),
DnsConfig: &sdtypes.DnsConfig{
NamespaceId: aws.String("private"),
RoutingPolicy: sdtypes.RoutingPolicyWeighted,
DnsRecords: []sdtypes.DnsRecord{{
Type: sdtypes.RecordTypeAaaa,
TTL: aws.Int64(100),
}},
},
},
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: services,
instances: make(map[string]map[string]*sdtypes.Instance),
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
expectedInstances := make(map[string]*sdtypes.Instance)
// IPv4-based instance
provider.RegisterInstance(context.Background(), services["private"]["a-srv"], &endpoint.Endpoint{
RecordType: endpoint.RecordTypeA,
DNSName: "service1.private.com.",
RecordTTL: 300,
Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5"},
})
expectedInstances["1.2.3.4"] = &sdtypes.Instance{
Id: aws.String("1.2.3.4"),
Attributes: map[string]string{
sdInstanceAttrIPV4: "1.2.3.4",
},
}
expectedInstances["1.2.3.5"] = &sdtypes.Instance{
Id: aws.String("1.2.3.5"),
Attributes: map[string]string{
sdInstanceAttrIPV4: "1.2.3.5",
},
}
// AWS ELB instance (ALIAS)
provider.RegisterInstance(context.Background(), services["private"]["alias-srv"], &endpoint.Endpoint{
RecordType: endpoint.RecordTypeCNAME,
DNSName: "service1.private.com.",
RecordTTL: 300,
Targets: endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com", "load-balancer.us-west-2.elb.amazonaws.com"},
})
expectedInstances["load-balancer.us-east-1.elb.amazonaws.com"] = &sdtypes.Instance{
Id: aws.String("load-balancer.us-east-1.elb.amazonaws.com"),
Attributes: map[string]string{
sdInstanceAttrAlias: "load-balancer.us-east-1.elb.amazonaws.com",
},
}
expectedInstances["load-balancer.us-west-2.elb.amazonaws.com"] = &sdtypes.Instance{
Id: aws.String("load-balancer.us-west-2.elb.amazonaws.com"),
Attributes: map[string]string{
sdInstanceAttrAlias: "load-balancer.us-west-2.elb.amazonaws.com",
},
}
// AWS NLB instance (ALIAS)
provider.RegisterInstance(context.Background(), services["private"]["alias-srv"], &endpoint.Endpoint{
RecordType: endpoint.RecordTypeCNAME,
DNSName: "service1.private.com.",
RecordTTL: 300,
Targets: endpoint.Targets{"load-balancer.elb.us-west-2.amazonaws.com"},
})
expectedInstances["load-balancer.elb.us-west-2.amazonaws.com"] = &sdtypes.Instance{
Id: aws.String("load-balancer.elb.us-west-2.amazonaws.com"),
Attributes: map[string]string{
sdInstanceAttrAlias: "load-balancer.elb.us-west-2.amazonaws.com",
},
}
// CNAME instance
provider.RegisterInstance(context.Background(), services["private"]["cname-srv"], &endpoint.Endpoint{
RecordType: endpoint.RecordTypeCNAME,
DNSName: "service2.private.com.",
RecordTTL: 300,
Targets: endpoint.Targets{"cname.target.com"},
})
expectedInstances["cname.target.com"] = &sdtypes.Instance{
Id: aws.String("cname.target.com"),
Attributes: map[string]string{
sdInstanceAttrCname: "cname.target.com",
},
}
// IPv6-based instance
provider.RegisterInstance(context.Background(), services["private"]["aaaa-srv"], &endpoint.Endpoint{
RecordType: endpoint.RecordTypeAAAA,
DNSName: "service4.private.com.",
RecordTTL: 300,
Targets: endpoint.Targets{"0000:0000:0000:0000:abcd:abcd:abcd:abcd"},
})
expectedInstances["0000:0000:0000:0000:abcd:abcd:abcd:abcd"] = &sdtypes.Instance{
Id: aws.String("0000:0000:0000:0000:abcd:abcd:abcd:abcd"),
Attributes: map[string]string{
sdInstanceAttrIPV6: "0000:0000:0000:0000:abcd:abcd:abcd:abcd",
},
}
// validate instances
for _, srvInst := range api.instances {
for id, inst := range srvInst {
if !reflect.DeepEqual(*expectedInstances[id], *inst) {
t.Errorf("Instances don't match, expected = %v, actual %v", *expectedInstances[id], *inst)
}
}
}
}
func TestAWSSDProvider_DeregisterInstance(t *testing.T) {
namespaces := map[string]*sdtypes.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: sdtypes.NamespaceTypeDnsPrivate,
},
}
services := map[string]map[string]*sdtypes.Service{
"private": {
"srv1": {
Id: aws.String("srv1"),
Name: aws.String("service1"),
},
},
}
instances := map[string]map[string]*sdtypes.Instance{
"srv1": {
"1.2.3.4": {
Id: aws.String("1.2.3.4"),
Attributes: map[string]string{
sdInstanceAttrIPV4: "1.2.3.4",
},
},
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: services,
instances: instances,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
provider.DeregisterInstance(context.Background(), services["private"]["srv1"], endpoint.NewEndpoint("srv1.private.com.", endpoint.RecordTypeA, "1.2.3.4"))
assert.Len(t, instances["srv1"], 0)
}
func TestAWSSDProvider_awsTags(t *testing.T) {
tests := []struct {
Expectation []sdtypes.Tag
Input map[string]string
}{
{
Expectation: []sdtypes.Tag{
{
Key: aws.String("key1"),
Value: aws.String("value1"),
},
{
Key: aws.String("key2"),
Value: aws.String("value2"),
},
},
Input: map[string]string{
"key1": "value1",
"key2": "value2",
},
},
{
Expectation: []sdtypes.Tag{},
Input: map[string]string{},
},
{
Expectation: []sdtypes.Tag{},
Input: nil,
},
}
for _, test := range tests {
require.ElementsMatch(t, test.Expectation, awsTags(test.Input))
}
}