mirror of
				https://github.com/kubernetes-sigs/external-dns.git
				synced 2025-10-31 02:31:00 +01:00 
			
		
		
		
	AWSSD Remove Empty Service
This commit is contained in:
		
							parent
							
								
									c82a3d2b10
								
							
						
					
					
						commit
						7a0601c8b4
					
				
							
								
								
									
										2
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								main.go
									
									
									
									
									
								
							| @ -204,7 +204,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": | ||||
|  | ||||
| @ -87,6 +87,7 @@ type Config struct { | ||||
| 	AWSAPIRetries                     int | ||||
| 	AWSPreferCNAME                    bool | ||||
| 	AWSZoneCacheDuration              time.Duration | ||||
| 	AWSSDServiceCleanup               bool | ||||
| 	AzureConfigFile                   string | ||||
| 	AzureResourceGroup                string | ||||
| 	AzureSubscriptionID               string | ||||
| @ -217,6 +218,7 @@ var defaultConfig = &Config{ | ||||
| 	AWSAPIRetries:               3, | ||||
| 	AWSPreferCNAME:              false, | ||||
| 	AWSZoneCacheDuration:        0 * time.Second, | ||||
| 	AWSSDServiceCleanup:         false, | ||||
| 	AzureConfigFile:             "/etc/kubernetes/azure.json", | ||||
| 	AzureResourceGroup:          "", | ||||
| 	AzureSubscriptionID:         "", | ||||
| @ -405,6 +407,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) | ||||
|  | ||||
| @ -63,6 +63,7 @@ var ( | ||||
| 		AWSAPIRetries:               3, | ||||
| 		AWSPreferCNAME:              false, | ||||
| 		AWSZoneCacheDuration:        0 * time.Second, | ||||
| 		AWSSDServiceCleanup:         false, | ||||
| 		AzureConfigFile:             "/etc/kubernetes/azure.json", | ||||
| 		AzureResourceGroup:          "", | ||||
| 		AzureSubscriptionID:         "", | ||||
| @ -152,6 +153,7 @@ var ( | ||||
| 		AWSAPIRetries:               13, | ||||
| 		AWSPreferCNAME:              true, | ||||
| 		AWSZoneCacheDuration:        10 * time.Second, | ||||
| 		AWSSDServiceCleanup:         true, | ||||
| 		AzureConfigFile:             "azure.json", | ||||
| 		AzureResourceGroup:          "arg", | ||||
| 		AzureSubscriptionID:         "arg", | ||||
| @ -302,6 +304,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", | ||||
| @ -402,6 +405,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", | ||||
|  | ||||
| @ -70,6 +70,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. | ||||
| @ -81,10 +82,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( | ||||
| @ -113,9 +118,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 | ||||
| @ -162,6 +169,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) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| @ -285,9 +298,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 | ||||
| @ -491,6 +503,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 { | ||||
|  | ||||
| @ -181,12 +181,30 @@ 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 | ||||
| 	} | ||||
| 
 | ||||
| 	// serviceInstances := s.instances[*input.ServiceId] | ||||
| 	// delete(serviceInstances, *input.InstanceId) | ||||
| 
 | ||||
| 	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 +306,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 +334,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 +394,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) | ||||
| @ -439,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 +513,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 +550,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 +654,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 +665,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 +771,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 +888,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")) | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user