mirror of
				https://github.com/kubernetes-sigs/external-dns.git
				synced 2025-11-04 04:31:00 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			370 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			370 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
Copyright 2020 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 scaleway
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"fmt"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	domain "github.com/scaleway/scaleway-sdk-go/api/domain/v2beta1"
 | 
						|
	"github.com/scaleway/scaleway-sdk-go/scw"
 | 
						|
	log "github.com/sirupsen/logrus"
 | 
						|
 | 
						|
	"sigs.k8s.io/external-dns/endpoint"
 | 
						|
	"sigs.k8s.io/external-dns/pkg/apis/externaldns"
 | 
						|
	"sigs.k8s.io/external-dns/plan"
 | 
						|
	"sigs.k8s.io/external-dns/provider"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	scalewyRecordTTL        uint32 = 300
 | 
						|
	scalewayDefaultPriority uint32 = 0
 | 
						|
	scalewayPriorityKey     string = "scw/priority"
 | 
						|
)
 | 
						|
 | 
						|
// ScalewayProvider implements the DNS provider for Scaleway DNS
 | 
						|
type ScalewayProvider struct {
 | 
						|
	provider.BaseProvider
 | 
						|
	domainAPI DomainAPI
 | 
						|
	dryRun    bool
 | 
						|
	// only consider hosted zones managing domains ending in this suffix
 | 
						|
	domainFilter endpoint.DomainFilter
 | 
						|
}
 | 
						|
 | 
						|
// ScalewayChange differentiates between ChangActions
 | 
						|
type ScalewayChange struct {
 | 
						|
	Action string
 | 
						|
	Record []domain.Record
 | 
						|
}
 | 
						|
 | 
						|
// NewScalewayProvider initializes a new Scaleway DNS provider
 | 
						|
func NewScalewayProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*ScalewayProvider, error) {
 | 
						|
	scwClient, err := scw.NewClient(
 | 
						|
		scw.WithEnv(),
 | 
						|
		scw.WithUserAgent("ExternalDNS/"+externaldns.Version),
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	if _, ok := scwClient.GetAccessKey(); !ok {
 | 
						|
		return nil, fmt.Errorf("access key no set")
 | 
						|
	}
 | 
						|
 | 
						|
	if _, ok := scwClient.GetSecretKey(); !ok {
 | 
						|
		return nil, fmt.Errorf("secret key no set")
 | 
						|
	}
 | 
						|
 | 
						|
	domainAPI := domain.NewAPI(scwClient)
 | 
						|
 | 
						|
	return &ScalewayProvider{
 | 
						|
		domainAPI:    domainAPI,
 | 
						|
		dryRun:       dryRun,
 | 
						|
		domainFilter: domainFilter,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
// AdjustEndpoints is used to normalize the endoints
 | 
						|
func (p *ScalewayProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
 | 
						|
	eps := make([]*endpoint.Endpoint, len(endpoints))
 | 
						|
	for i := range endpoints {
 | 
						|
		eps[i] = endpoints[i]
 | 
						|
		if !eps[i].RecordTTL.IsConfigured() {
 | 
						|
			eps[i].RecordTTL = endpoint.TTL(scalewyRecordTTL)
 | 
						|
		}
 | 
						|
		if _, ok := eps[i].GetProviderSpecificProperty(scalewayPriorityKey); !ok {
 | 
						|
			eps[i] = eps[i].WithProviderSpecific(scalewayPriorityKey, fmt.Sprintf("%d", scalewayDefaultPriority))
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return eps
 | 
						|
}
 | 
						|
 | 
						|
// Zones returns the list of hosted zones.
 | 
						|
func (p *ScalewayProvider) Zones(ctx context.Context) ([]*domain.DNSZone, error) {
 | 
						|
	res := []*domain.DNSZone{}
 | 
						|
 | 
						|
	dnsZones, err := p.domainAPI.ListDNSZones(&domain.ListDNSZonesRequest{}, scw.WithAllPages(), scw.WithContext(ctx))
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	for _, dnsZone := range dnsZones.DNSZones {
 | 
						|
		if p.domainFilter.Match(getCompleteZoneName(dnsZone)) {
 | 
						|
			res = append(res, dnsZone)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return res, nil
 | 
						|
}
 | 
						|
 | 
						|
// Records returns the list of records in a given zone.
 | 
						|
func (p *ScalewayProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
 | 
						|
	endpoints := map[string]*endpoint.Endpoint{}
 | 
						|
	dnsZones, err := p.Zones(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	for _, zone := range dnsZones {
 | 
						|
		recordsResp, err := p.domainAPI.ListDNSZoneRecords(&domain.ListDNSZoneRecordsRequest{
 | 
						|
			DNSZone: getCompleteZoneName(zone),
 | 
						|
		}, scw.WithAllPages())
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
 | 
						|
		for _, record := range recordsResp.Records {
 | 
						|
			name := record.Name + "."
 | 
						|
 | 
						|
			// trim any leading or ending dot
 | 
						|
			fullRecordName := strings.Trim(name+getCompleteZoneName(zone), ".")
 | 
						|
 | 
						|
			if !provider.SupportedRecordType(record.Type.String()) {
 | 
						|
				log.Infof("Skipping record %s because type %s is not supported", fullRecordName, record.Type.String())
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			// in external DNS, same endpoint have the same ttl and same priority
 | 
						|
			// it's not the case in Scaleway DNS. It should never happen, but if
 | 
						|
			// the record is modified without going through ExternalDNS, we could have
 | 
						|
			// different priorities of ttls for a same name.
 | 
						|
			// In this case, we juste take the first one.
 | 
						|
			if existingEndpoint, ok := endpoints[record.Type.String()+"/"+fullRecordName]; ok {
 | 
						|
				existingEndpoint.Targets = append(existingEndpoint.Targets, record.Data)
 | 
						|
				log.Infof("Appending target %s to record %s, using TTL and priority of target %s", record.Data, fullRecordName, existingEndpoint.Targets[0])
 | 
						|
			} else {
 | 
						|
				ep := endpoint.NewEndpointWithTTL(fullRecordName, record.Type.String(), endpoint.TTL(record.TTL), record.Data)
 | 
						|
				ep = ep.WithProviderSpecific(scalewayPriorityKey, fmt.Sprintf("%d", record.Priority))
 | 
						|
				endpoints[record.Type.String()+"/"+fullRecordName] = ep
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	returnedEndpoints := []*endpoint.Endpoint{}
 | 
						|
	for _, ep := range endpoints {
 | 
						|
		returnedEndpoints = append(returnedEndpoints, ep)
 | 
						|
	}
 | 
						|
 | 
						|
	return returnedEndpoints, nil
 | 
						|
}
 | 
						|
 | 
						|
// ApplyChanges applies a set of changes in a zone.
 | 
						|
func (p *ScalewayProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
 | 
						|
	requests, err := p.generateApplyRequests(ctx, changes)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	for _, req := range requests {
 | 
						|
		logChanges(req)
 | 
						|
		if p.dryRun {
 | 
						|
			log.Info("Running in dry run mode")
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		_, err := p.domainAPI.UpdateDNSZoneRecords(req, scw.WithContext(ctx))
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (p *ScalewayProvider) generateApplyRequests(ctx context.Context, changes *plan.Changes) ([]*domain.UpdateDNSZoneRecordsRequest, error) {
 | 
						|
	returnedRequests := []*domain.UpdateDNSZoneRecordsRequest{}
 | 
						|
	recordsToAdd := map[string]*domain.RecordChangeAdd{}
 | 
						|
	recordsToDelete := map[string][]*domain.RecordChange{}
 | 
						|
 | 
						|
	dnsZones, err := p.Zones(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	zoneNameMapper := provider.ZoneIDName{}
 | 
						|
	for _, zone := range dnsZones {
 | 
						|
		zoneName := getCompleteZoneName(zone)
 | 
						|
		zoneNameMapper.Add(zoneName, zoneName)
 | 
						|
		recordsToAdd[zoneName] = &domain.RecordChangeAdd{
 | 
						|
			Records: []*domain.Record{},
 | 
						|
		}
 | 
						|
		recordsToDelete[zoneName] = []*domain.RecordChange{}
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debugf("Following records present in updateOld")
 | 
						|
	for _, c := range changes.UpdateOld {
 | 
						|
		zone, _ := zoneNameMapper.FindZone(c.DNSName)
 | 
						|
		if zone == "" {
 | 
						|
			log.Infof("Ignore record %s since it's not handled by ExternalDNS", c.DNSName)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		recordsToDelete[zone] = append(recordsToDelete[zone], endpointToScalewayRecordsChangeDelete(zone, c)...)
 | 
						|
		log.Debugf("%s", c.String())
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debugf("Following records present in delete")
 | 
						|
	for _, c := range changes.Delete {
 | 
						|
		zone, _ := zoneNameMapper.FindZone(c.DNSName)
 | 
						|
		if zone == "" {
 | 
						|
			log.Infof("Ignore record %s since it's not handled by ExternalDNS", c.DNSName)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		recordsToDelete[zone] = append(recordsToDelete[zone], endpointToScalewayRecordsChangeDelete(zone, c)...)
 | 
						|
		log.Debugf("%s", c.String())
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debugf("Following records present in create")
 | 
						|
	for _, c := range changes.Create {
 | 
						|
		zone, _ := zoneNameMapper.FindZone(c.DNSName)
 | 
						|
		if zone == "" {
 | 
						|
			log.Infof("Ignore record %s since it's not handled by ExternalDNS", c.DNSName)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		recordsToAdd[zone].Records = append(recordsToAdd[zone].Records, endpointToScalewayRecords(zone, c)...)
 | 
						|
		log.Debugf("%s", c.String())
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debugf("Following records present in updateNew")
 | 
						|
	for _, c := range changes.UpdateNew {
 | 
						|
		zone, _ := zoneNameMapper.FindZone(c.DNSName)
 | 
						|
		if zone == "" {
 | 
						|
			log.Infof("Ignore record %s since it's not handled by ExternalDNS", c.DNSName)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		recordsToAdd[zone].Records = append(recordsToAdd[zone].Records, endpointToScalewayRecords(zone, c)...)
 | 
						|
		log.Debugf("%s", c.String())
 | 
						|
	}
 | 
						|
 | 
						|
	for _, zone := range dnsZones {
 | 
						|
		zoneName := getCompleteZoneName(zone)
 | 
						|
		req := &domain.UpdateDNSZoneRecordsRequest{
 | 
						|
			DNSZone: zoneName,
 | 
						|
			Changes: recordsToDelete[zoneName],
 | 
						|
		}
 | 
						|
		req.Changes = append(req.Changes, &domain.RecordChange{
 | 
						|
			Add: recordsToAdd[zoneName],
 | 
						|
		})
 | 
						|
		returnedRequests = append(returnedRequests, req)
 | 
						|
	}
 | 
						|
 | 
						|
	return returnedRequests, nil
 | 
						|
}
 | 
						|
 | 
						|
func getCompleteZoneName(zone *domain.DNSZone) string {
 | 
						|
	subdomain := zone.Subdomain + "."
 | 
						|
	if zone.Subdomain == "" {
 | 
						|
		subdomain = ""
 | 
						|
	}
 | 
						|
	return subdomain + zone.Domain
 | 
						|
}
 | 
						|
 | 
						|
func endpointToScalewayRecords(zoneName string, ep *endpoint.Endpoint) []*domain.Record {
 | 
						|
	// no annotation results in a TTL of 0, default to 300 for consistency with other providers
 | 
						|
	ttl := scalewyRecordTTL
 | 
						|
	if ep.RecordTTL.IsConfigured() {
 | 
						|
		ttl = uint32(ep.RecordTTL)
 | 
						|
	}
 | 
						|
	priority := scalewayDefaultPriority
 | 
						|
	if prop, ok := ep.GetProviderSpecificProperty(scalewayPriorityKey); ok {
 | 
						|
		prio, err := strconv.ParseUint(prop.Value, 10, 32)
 | 
						|
		if err != nil {
 | 
						|
			log.Errorf("Failed parsing value of %s: %s: %v; using priority of %d", scalewayPriorityKey, prop.Value, err, scalewayDefaultPriority)
 | 
						|
		} else {
 | 
						|
			priority = uint32(prio)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	records := []*domain.Record{}
 | 
						|
 | 
						|
	for _, target := range ep.Targets {
 | 
						|
		finalTargetName := target
 | 
						|
		if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
 | 
						|
			finalTargetName = provider.EnsureTrailingDot(target)
 | 
						|
		}
 | 
						|
 | 
						|
		records = append(records, &domain.Record{
 | 
						|
			Data:     finalTargetName,
 | 
						|
			Name:     strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
 | 
						|
			Priority: priority,
 | 
						|
			TTL:      ttl,
 | 
						|
			Type:     domain.RecordType(ep.RecordType),
 | 
						|
		})
 | 
						|
	}
 | 
						|
 | 
						|
	return records
 | 
						|
}
 | 
						|
 | 
						|
func endpointToScalewayRecordsChangeDelete(zoneName string, ep *endpoint.Endpoint) []*domain.RecordChange {
 | 
						|
	records := []*domain.RecordChange{}
 | 
						|
 | 
						|
	for _, target := range ep.Targets {
 | 
						|
		finalTargetName := target
 | 
						|
		if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
 | 
						|
			finalTargetName = provider.EnsureTrailingDot(target)
 | 
						|
		}
 | 
						|
 | 
						|
		records = append(records, &domain.RecordChange{
 | 
						|
			Delete: &domain.RecordChangeDelete{
 | 
						|
				IDFields: &domain.RecordIdentifier{
 | 
						|
					Data: &finalTargetName,
 | 
						|
					Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
 | 
						|
					Type: domain.RecordType(ep.RecordType),
 | 
						|
				},
 | 
						|
			},
 | 
						|
		})
 | 
						|
	}
 | 
						|
 | 
						|
	return records
 | 
						|
}
 | 
						|
 | 
						|
func logChanges(req *domain.UpdateDNSZoneRecordsRequest) {
 | 
						|
	if !log.IsLevelEnabled(log.InfoLevel) {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	log.Infof("Updating zone %s", req.DNSZone)
 | 
						|
	for _, change := range req.Changes {
 | 
						|
		if change.Add != nil {
 | 
						|
			for _, add := range change.Add.Records {
 | 
						|
				name := add.Name + "."
 | 
						|
				if add.Name == "" {
 | 
						|
					name = ""
 | 
						|
				}
 | 
						|
 | 
						|
				logFields := log.Fields{
 | 
						|
					"record":   name + req.DNSZone,
 | 
						|
					"type":     add.Type.String(),
 | 
						|
					"ttl":      add.TTL,
 | 
						|
					"priority": add.Priority,
 | 
						|
					"data":     add.Data,
 | 
						|
				}
 | 
						|
				log.WithFields(logFields).Info("Adding record")
 | 
						|
			}
 | 
						|
		} else if change.Delete != nil {
 | 
						|
			name := change.Delete.IDFields.Name + "."
 | 
						|
			if change.Delete.IDFields.Name == "" {
 | 
						|
				name = ""
 | 
						|
			}
 | 
						|
 | 
						|
			logFields := log.Fields{
 | 
						|
				"record": name + req.DNSZone,
 | 
						|
				"type":   change.Delete.IDFields.Type.String(),
 | 
						|
				"data":   *change.Delete.IDFields.Data,
 | 
						|
			}
 | 
						|
 | 
						|
			log.WithFields(logFields).Info("Deleting record")
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |