Merge pull request #2510 from voro015/aws-sd-delete-empty-services

AWSSD: Cleanup empty Services
This commit is contained in:
Kubernetes Prow Robot 2022-02-12 04:39:46 -08:00 committed by GitHub
commit 06405ed29c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 122 additions and 17 deletions

View File

@ -206,7 +206,7 @@ func main() {
log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry)
cfg.Registry = "aws-sd"
}
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.DryRun)
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID)
case "azure-dns", "azure":
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "azure-private-dns":

View File

@ -87,6 +87,7 @@ type Config struct {
AWSAPIRetries int
AWSPreferCNAME bool
AWSZoneCacheDuration time.Duration
AWSSDServiceCleanup bool
AzureConfigFile string
AzureResourceGroup string
AzureSubscriptionID string
@ -226,6 +227,7 @@ var defaultConfig = &Config{
AWSAPIRetries: 3,
AWSPreferCNAME: false,
AWSZoneCacheDuration: 0 * time.Second,
AWSSDServiceCleanup: false,
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
AzureSubscriptionID: "",
@ -417,6 +419,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("aws-api-retries", "When using the AWS provider, set the maximum number of retries for API calls before giving up.").Default(strconv.Itoa(defaultConfig.AWSAPIRetries)).IntVar(&cfg.AWSAPIRetries)
app.Flag("aws-prefer-cname", "When using the AWS provider, prefer using CNAME instead of ALIAS (default: disabled)").BoolVar(&cfg.AWSPreferCNAME)
app.Flag("aws-zones-cache-duration", "When using the AWS provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.AWSZoneCacheDuration.String()).DurationVar(&cfg.AWSZoneCacheDuration)
app.Flag("aws-sd-service-cleanup", "When using the AWS CloudMap provider, delete empty Services without endpoints (default: disabled)").BoolVar(&cfg.AWSSDServiceCleanup)
app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile)
app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (required when --provider=azure-private-dns)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup)
app.Flag("azure-subscription-id", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure-private-dns)").Default(defaultConfig.AzureSubscriptionID).StringVar(&cfg.AzureSubscriptionID)

View File

@ -63,6 +63,7 @@ var (
AWSAPIRetries: 3,
AWSPreferCNAME: false,
AWSZoneCacheDuration: 0 * time.Second,
AWSSDServiceCleanup: false,
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
AzureSubscriptionID: "",
@ -160,6 +161,7 @@ var (
AWSAPIRetries: 13,
AWSPreferCNAME: true,
AWSZoneCacheDuration: 10 * time.Second,
AWSSDServiceCleanup: true,
AzureConfigFile: "azure.json",
AzureResourceGroup: "arg",
AzureSubscriptionID: "arg",
@ -325,6 +327,7 @@ func TestParseFlags(t *testing.T) {
"--aws-api-retries=13",
"--aws-prefer-cname",
"--aws-zones-cache-duration=10s",
"--aws-sd-service-cleanup",
"--no-aws-evaluate-target-health",
"--policy=upsert-only",
"--registry=noop",
@ -435,6 +438,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AWS_API_RETRIES": "13",
"EXTERNAL_DNS_AWS_PREFER_CNAME": "true",
"EXTERNAL_DNS_AWS_ZONES_CACHE_DURATION": "10s",
"EXTERNAL_DNS_AWS_SD_SERVICE_CLEANUP": "true",
"EXTERNAL_DNS_POLICY": "upsert-only",
"EXTERNAL_DNS_REGISTRY": "noop",
"EXTERNAL_DNS_TXT_OWNER_ID": "owner-1",

View File

@ -69,6 +69,7 @@ type AWSSDClient interface {
ListServicesPages(input *sd.ListServicesInput, fn func(*sd.ListServicesOutput, bool) bool) error
RegisterInstance(input *sd.RegisterInstanceInput) (*sd.RegisterInstanceOutput, error)
UpdateService(input *sd.UpdateServiceInput) (*sd.UpdateServiceOutput, error)
DeleteService(input *sd.DeleteServiceInput) (*sd.DeleteServiceOutput, error)
}
// AWSSDProvider is an implementation of Provider for AWS Cloud Map.
@ -80,10 +81,14 @@ type AWSSDProvider struct {
namespaceFilter endpoint.DomainFilter
// filter namespace by type (private or public)
namespaceTypeFilter *sd.NamespaceFilter
// enables service without instances cleanup
cleanEmptyService bool
// filter services for removal
ownerID string
}
// NewAWSSDProvider initializes a new AWS Cloud Map based Provider.
func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string, assumeRole string, dryRun bool) (*AWSSDProvider, error) {
func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string, assumeRole string, dryRun, cleanEmptyService bool, ownerID string) (*AWSSDProvider, error) {
config := aws.NewConfig()
config = config.WithHTTPClient(
@ -112,9 +117,11 @@ func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string,
provider := &AWSSDProvider{
client: sd.New(sess),
dryRun: dryRun,
namespaceFilter: domainFilter,
namespaceTypeFilter: newSdNamespaceFilter(namespaceType),
dryRun: dryRun,
cleanEmptyService: cleanEmptyService,
ownerID: ownerID,
}
return provider, nil
@ -161,6 +168,12 @@ func (p *AWSSDProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp
ep := p.instancesToEndpoint(ns, srv, instances)
endpoints = append(endpoints, ep)
}
if len(instances) == 0 {
err = p.DeleteService(srv)
if err != nil {
log.Warnf("Failed to delete service \"%s\", error: %s", aws.StringValue(srv.Name), err)
}
}
}
}
@ -284,9 +297,8 @@ func (p *AWSSDProvider) submitCreates(namespaces []*sd.NamespaceSummary, changes
}
// update local list of services
services[*srv.Name] = srv
} else if (ch.RecordTTL.IsConfigured() && *srv.DnsConfig.DnsRecords[0].TTL != int64(ch.RecordTTL)) ||
aws.StringValue(srv.Description) != ch.Labels[endpoint.AWSSDDescriptionLabel] {
// update service when TTL or Description differ
} else if ch.RecordTTL.IsConfigured() && *srv.DnsConfig.DnsRecords[0].TTL != int64(ch.RecordTTL) {
// update service when TTL differ
err = p.UpdateService(srv, ch)
if err != nil {
return err
@ -483,6 +495,27 @@ func (p *AWSSDProvider) UpdateService(service *sd.Service, ep *endpoint.Endpoint
return nil
}
// DeleteService deletes empty Service from AWS API if its owner id match
func (p *AWSSDProvider) DeleteService(service *sd.Service) error {
log.Debugf("Check if service \"%s\" owner id match and it can be deleted", *service.Name)
if !p.dryRun && p.cleanEmptyService {
// convert ownerID string to service description format
label := endpoint.NewLabels()
label[endpoint.OwnerLabelKey] = p.ownerID
label[endpoint.AWSSDDescriptionLabel] = label.Serialize(false)
if aws.StringValue(service.Description) == label[endpoint.AWSSDDescriptionLabel] {
log.Infof("Deleting service \"%s\"", *service.Name)
_, err := p.client.DeleteService(&sd.DeleteServiceInput{
Id: aws.String(*service.Id),
})
return err
}
log.Debugf("Skipping service removal %s because owner id does not match, found: \"%s\", required: \"%s\"", aws.StringValue(service.Name), aws.StringValue(service.Description), label[endpoint.AWSSDDescriptionLabel])
}
return nil
}
// RegisterInstance creates a new instance in given service.
func (p *AWSSDProvider) RegisterInstance(service *sd.Service, ep *endpoint.Endpoint) error {
for _, target := range ep.Targets {

View File

@ -181,12 +181,27 @@ func (s *AWSSDClientStub) UpdateService(input *sd.UpdateServiceInput) (*sd.Updat
return &sd.UpdateServiceOutput{}, nil
}
func newTestAWSSDProvider(api AWSSDClient, domainFilter endpoint.DomainFilter, namespaceTypeFilter string) *AWSSDProvider {
func (s *AWSSDClientStub) DeleteService(input *sd.DeleteServiceInput) (*sd.DeleteServiceOutput, error) {
out, err := s.GetService(&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),
dryRun: false,
cleanEmptyService: true,
ownerID: ownerID,
}
}
@ -288,7 +303,7 @@ func TestAWSSDProvider_Records(t *testing.T) {
instances: instances,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
endpoints, _ := provider.Records(context.Background())
@ -316,7 +331,7 @@ func TestAWSSDProvider_ApplyChanges(t *testing.T) {
{DNSName: "service3.private.com", Targets: endpoint.Targets{"cname.target.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 100},
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
ctx := context.Background()
@ -376,7 +391,7 @@ func TestAWSSDProvider_ListNamespaces(t *testing.T) {
{"domain filter", endpoint.NewDomainFilter([]string{"public.com"}), "", []*sd.NamespaceSummary{namespaceToNamespaceSummary(namespaces["public"])}},
{"non-existing domain", endpoint.NewDomainFilter([]string{"xxx.com"}), "", []*sd.NamespaceSummary{}},
} {
provider := newTestAWSSDProvider(api, tc.domainFilter, tc.namespaceTypeFilter)
provider := newTestAWSSDProvider(api, tc.domainFilter, tc.namespaceTypeFilter, "")
result, err := provider.ListNamespaces()
require.NoError(t, err)
@ -442,7 +457,7 @@ func TestAWSSDProvider_ListServicesByNamespace(t *testing.T) {
}{
{map[string]*sd.Service{"service1": services["private"]["srv1"], "service2": services["private"]["srv2"]}},
} {
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
result, err := provider.ListServicesByNamespaceID(namespaces["private"].Id)
require.NoError(t, err)
@ -495,7 +510,7 @@ func TestAWSSDProvider_ListInstancesByService(t *testing.T) {
instances: instances,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
result, err := provider.ListInstancesByServiceID(services["private"]["srv1"].Id)
require.NoError(t, err)
@ -532,7 +547,7 @@ func TestAWSSDProvider_CreateService(t *testing.T) {
expectedServices := make(map[string]*sd.Service)
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
// A type
provider.CreateService(aws.String("private"), aws.String("A-srv"), &endpoint.Endpoint{
@ -636,7 +651,7 @@ func TestAWSSDProvider_UpdateService(t *testing.T) {
services: services,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
// update service with different TTL
provider.UpdateService(services["private"]["srv1"], &endpoint.Endpoint{
@ -647,6 +662,56 @@ func TestAWSSDProvider_UpdateService(t *testing.T) {
assert.Equal(t, int64(100), *api.services["private"]["srv1"].DnsConfig.DnsRecords[0].TTL)
}
func TestAWSSDProvider_DeleteService(t *testing.T) {
namespaces := map[string]*sd.Namespace{
"private": {
Id: aws.String("private"),
Name: aws.String("private.com"),
Type: aws.String(sd.NamespaceTypeDnsPrivate),
},
}
services := map[string]map[string]*sd.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"),
},
},
}
api := &AWSSDClientStub{
namespaces: namespaces,
services: services,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "owner-id")
// delete fist service
err := provider.DeleteService(services["private"]["srv1"])
assert.NoError(t, err)
assert.Len(t, api.services["private"], 1)
expectedServices := map[string]*sd.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]*sd.Namespace{
"private": {
@ -703,7 +768,7 @@ func TestAWSSDProvider_RegisterInstance(t *testing.T) {
instances: make(map[string]map[string]*sd.Instance),
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
expectedInstances := make(map[string]*sd.Instance)
@ -820,7 +885,7 @@ func TestAWSSDProvider_DeregisterInstance(t *testing.T) {
instances: instances,
}
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "")
provider := newTestAWSSDProvider(api, endpoint.NewDomainFilter([]string{}), "", "")
provider.DeregisterInstance(services["private"]["srv1"], endpoint.NewEndpoint("srv1.private.com.", endpoint.RecordTypeA, "1.2.3.4"))