sigs.k8s.io/external-dns@v0.14.1/provider/scaleway/scaleway.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package scaleway
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"strconv"
    24  	"strings"
    25  
    26  	domain "github.com/scaleway/scaleway-sdk-go/api/domain/v2beta1"
    27  	"github.com/scaleway/scaleway-sdk-go/scw"
    28  	log "github.com/sirupsen/logrus"
    29  
    30  	"sigs.k8s.io/external-dns/endpoint"
    31  	"sigs.k8s.io/external-dns/pkg/apis/externaldns"
    32  	"sigs.k8s.io/external-dns/plan"
    33  	"sigs.k8s.io/external-dns/provider"
    34  )
    35  
    36  const (
    37  	scalewyRecordTTL        uint32 = 300
    38  	scalewayDefaultPriority uint32 = 0
    39  	scalewayPriorityKey     string = "scw/priority"
    40  )
    41  
    42  // ScalewayProvider implements the DNS provider for Scaleway DNS
    43  type ScalewayProvider struct {
    44  	provider.BaseProvider
    45  	domainAPI DomainAPI
    46  	dryRun    bool
    47  	// only consider hosted zones managing domains ending in this suffix
    48  	domainFilter endpoint.DomainFilter
    49  }
    50  
    51  // ScalewayChange differentiates between ChangActions
    52  type ScalewayChange struct {
    53  	Action string
    54  	Record []domain.Record
    55  }
    56  
    57  // NewScalewayProvider initializes a new Scaleway DNS provider
    58  func NewScalewayProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*ScalewayProvider, error) {
    59  	var err error
    60  	defaultPageSize := uint64(1000)
    61  	if envPageSize, ok := os.LookupEnv("SCW_DEFAULT_PAGE_SIZE"); ok {
    62  		defaultPageSize, err = strconv.ParseUint(envPageSize, 10, 32)
    63  		if err != nil {
    64  			log.Infof("Ignoring default page size %s, defaulting to 1000", envPageSize)
    65  			defaultPageSize = 1000
    66  		}
    67  	}
    68  
    69  	p := &scw.Profile{}
    70  	c, err := scw.LoadConfig()
    71  	if err != nil {
    72  		log.Warnf("Cannot load config: %v", err)
    73  	} else {
    74  		p, err = c.GetActiveProfile()
    75  		if err != nil {
    76  			log.Warnf("Cannot get active profile: %v", err)
    77  		}
    78  	}
    79  
    80  	scwClient, err := scw.NewClient(
    81  		scw.WithProfile(p),
    82  		scw.WithEnv(),
    83  		scw.WithUserAgent("ExternalDNS/"+externaldns.Version),
    84  		scw.WithDefaultPageSize(uint32(defaultPageSize)),
    85  	)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	if _, ok := scwClient.GetAccessKey(); !ok {
    91  		return nil, fmt.Errorf("access key no set")
    92  	}
    93  
    94  	if _, ok := scwClient.GetSecretKey(); !ok {
    95  		return nil, fmt.Errorf("secret key no set")
    96  	}
    97  
    98  	domainAPI := domain.NewAPI(scwClient)
    99  
   100  	return &ScalewayProvider{
   101  		domainAPI:    domainAPI,
   102  		dryRun:       dryRun,
   103  		domainFilter: domainFilter,
   104  	}, nil
   105  }
   106  
   107  // AdjustEndpoints is used to normalize the endoints
   108  func (p *ScalewayProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) {
   109  	eps := make([]*endpoint.Endpoint, len(endpoints))
   110  	for i := range endpoints {
   111  		eps[i] = endpoints[i]
   112  		if !eps[i].RecordTTL.IsConfigured() {
   113  			eps[i].RecordTTL = endpoint.TTL(scalewyRecordTTL)
   114  		}
   115  		if _, ok := eps[i].GetProviderSpecificProperty(scalewayPriorityKey); !ok {
   116  			eps[i] = eps[i].WithProviderSpecific(scalewayPriorityKey, fmt.Sprintf("%d", scalewayDefaultPriority))
   117  		}
   118  	}
   119  	return eps, nil
   120  }
   121  
   122  // Zones returns the list of hosted zones.
   123  func (p *ScalewayProvider) Zones(ctx context.Context) ([]*domain.DNSZone, error) {
   124  	res := []*domain.DNSZone{}
   125  
   126  	dnsZones, err := p.domainAPI.ListDNSZones(&domain.ListDNSZonesRequest{}, scw.WithAllPages(), scw.WithContext(ctx))
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	for _, dnsZone := range dnsZones.DNSZones {
   132  		if p.domainFilter.Match(getCompleteZoneName(dnsZone)) {
   133  			res = append(res, dnsZone)
   134  		}
   135  	}
   136  
   137  	return res, nil
   138  }
   139  
   140  // Records returns the list of records in a given zone.
   141  func (p *ScalewayProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
   142  	endpoints := map[string]*endpoint.Endpoint{}
   143  	dnsZones, err := p.Zones(ctx)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	for _, zone := range dnsZones {
   149  		recordsResp, err := p.domainAPI.ListDNSZoneRecords(&domain.ListDNSZoneRecordsRequest{
   150  			DNSZone: getCompleteZoneName(zone),
   151  		}, scw.WithAllPages())
   152  		if err != nil {
   153  			return nil, err
   154  		}
   155  
   156  		for _, record := range recordsResp.Records {
   157  			name := record.Name + "."
   158  
   159  			// trim any leading or ending dot
   160  			fullRecordName := strings.Trim(name+getCompleteZoneName(zone), ".")
   161  
   162  			if !provider.SupportedRecordType(record.Type.String()) {
   163  				log.Infof("Skipping record %s because type %s is not supported", fullRecordName, record.Type.String())
   164  				continue
   165  			}
   166  
   167  			// in external DNS, same endpoint have the same ttl and same priority
   168  			// it's not the case in Scaleway DNS. It should never happen, but if
   169  			// the record is modified without going through ExternalDNS, we could have
   170  			// different priorities of ttls for a same name.
   171  			// In this case, we juste take the first one.
   172  			if existingEndpoint, ok := endpoints[record.Type.String()+"/"+fullRecordName]; ok {
   173  				existingEndpoint.Targets = append(existingEndpoint.Targets, record.Data)
   174  				log.Infof("Appending target %s to record %s, using TTL and priority of target %s", record.Data, fullRecordName, existingEndpoint.Targets[0])
   175  			} else {
   176  				ep := endpoint.NewEndpointWithTTL(fullRecordName, record.Type.String(), endpoint.TTL(record.TTL), record.Data)
   177  				ep = ep.WithProviderSpecific(scalewayPriorityKey, fmt.Sprintf("%d", record.Priority))
   178  				endpoints[record.Type.String()+"/"+fullRecordName] = ep
   179  			}
   180  		}
   181  	}
   182  	returnedEndpoints := []*endpoint.Endpoint{}
   183  	for _, ep := range endpoints {
   184  		returnedEndpoints = append(returnedEndpoints, ep)
   185  	}
   186  
   187  	return returnedEndpoints, nil
   188  }
   189  
   190  // ApplyChanges applies a set of changes in a zone.
   191  func (p *ScalewayProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
   192  	requests, err := p.generateApplyRequests(ctx, changes)
   193  	if err != nil {
   194  		return err
   195  	}
   196  	for _, req := range requests {
   197  		logChanges(req)
   198  		if p.dryRun {
   199  			log.Info("Running in dry run mode")
   200  			continue
   201  		}
   202  		_, err := p.domainAPI.UpdateDNSZoneRecords(req, scw.WithContext(ctx))
   203  		if err != nil {
   204  			return err
   205  		}
   206  	}
   207  	return nil
   208  }
   209  
   210  func (p *ScalewayProvider) generateApplyRequests(ctx context.Context, changes *plan.Changes) ([]*domain.UpdateDNSZoneRecordsRequest, error) {
   211  	returnedRequests := []*domain.UpdateDNSZoneRecordsRequest{}
   212  	recordsToAdd := map[string]*domain.RecordChangeAdd{}
   213  	recordsToDelete := map[string][]*domain.RecordChange{}
   214  
   215  	dnsZones, err := p.Zones(ctx)
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  
   220  	zoneNameMapper := provider.ZoneIDName{}
   221  	for _, zone := range dnsZones {
   222  		zoneName := getCompleteZoneName(zone)
   223  		zoneNameMapper.Add(zoneName, zoneName)
   224  		recordsToAdd[zoneName] = &domain.RecordChangeAdd{
   225  			Records: []*domain.Record{},
   226  		}
   227  		recordsToDelete[zoneName] = []*domain.RecordChange{}
   228  	}
   229  
   230  	log.Debugf("Following records present in updateOld")
   231  	for _, c := range changes.UpdateOld {
   232  		zone, _ := zoneNameMapper.FindZone(c.DNSName)
   233  		if zone == "" {
   234  			log.Infof("Ignore record %s since it's not handled by ExternalDNS", c.DNSName)
   235  			continue
   236  		}
   237  		recordsToDelete[zone] = append(recordsToDelete[zone], endpointToScalewayRecordsChangeDelete(zone, c)...)
   238  		log.Debugf("%s", c.String())
   239  	}
   240  
   241  	log.Debugf("Following records present in delete")
   242  	for _, c := range changes.Delete {
   243  		zone, _ := zoneNameMapper.FindZone(c.DNSName)
   244  		if zone == "" {
   245  			log.Infof("Ignore record %s since it's not handled by ExternalDNS", c.DNSName)
   246  			continue
   247  		}
   248  		recordsToDelete[zone] = append(recordsToDelete[zone], endpointToScalewayRecordsChangeDelete(zone, c)...)
   249  		log.Debugf("%s", c.String())
   250  	}
   251  
   252  	log.Debugf("Following records present in create")
   253  	for _, c := range changes.Create {
   254  		zone, _ := zoneNameMapper.FindZone(c.DNSName)
   255  		if zone == "" {
   256  			log.Infof("Ignore record %s since it's not handled by ExternalDNS", c.DNSName)
   257  			continue
   258  		}
   259  		recordsToAdd[zone].Records = append(recordsToAdd[zone].Records, endpointToScalewayRecords(zone, c)...)
   260  		log.Debugf("%s", c.String())
   261  	}
   262  
   263  	log.Debugf("Following records present in updateNew")
   264  	for _, c := range changes.UpdateNew {
   265  		zone, _ := zoneNameMapper.FindZone(c.DNSName)
   266  		if zone == "" {
   267  			log.Infof("Ignore record %s since it's not handled by ExternalDNS", c.DNSName)
   268  			continue
   269  		}
   270  		recordsToAdd[zone].Records = append(recordsToAdd[zone].Records, endpointToScalewayRecords(zone, c)...)
   271  		log.Debugf("%s", c.String())
   272  	}
   273  
   274  	for _, zone := range dnsZones {
   275  		zoneName := getCompleteZoneName(zone)
   276  		req := &domain.UpdateDNSZoneRecordsRequest{
   277  			DNSZone: zoneName,
   278  			Changes: recordsToDelete[zoneName],
   279  		}
   280  		req.Changes = append(req.Changes, &domain.RecordChange{
   281  			Add: recordsToAdd[zoneName],
   282  		})
   283  		// ignore sending empty update requests
   284  		if len(req.Changes) == 1 && len(req.Changes[0].Add.Records) == 0 {
   285  			continue
   286  		}
   287  		returnedRequests = append(returnedRequests, req)
   288  	}
   289  
   290  	return returnedRequests, nil
   291  }
   292  
   293  func getCompleteZoneName(zone *domain.DNSZone) string {
   294  	subdomain := zone.Subdomain + "."
   295  	if zone.Subdomain == "" {
   296  		subdomain = ""
   297  	}
   298  	return subdomain + zone.Domain
   299  }
   300  
   301  func endpointToScalewayRecords(zoneName string, ep *endpoint.Endpoint) []*domain.Record {
   302  	// no annotation results in a TTL of 0, default to 300 for consistency with other providers
   303  	ttl := scalewyRecordTTL
   304  	if ep.RecordTTL.IsConfigured() {
   305  		ttl = uint32(ep.RecordTTL)
   306  	}
   307  	priority := scalewayDefaultPriority
   308  	if prop, ok := ep.GetProviderSpecificProperty(scalewayPriorityKey); ok {
   309  		prio, err := strconv.ParseUint(prop, 10, 32)
   310  		if err != nil {
   311  			log.Errorf("Failed parsing value of %s: %s: %v; using priority of %d", scalewayPriorityKey, prop, err, scalewayDefaultPriority)
   312  		} else {
   313  			priority = uint32(prio)
   314  		}
   315  	}
   316  
   317  	records := []*domain.Record{}
   318  
   319  	for _, target := range ep.Targets {
   320  		finalTargetName := target
   321  		if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
   322  			finalTargetName = provider.EnsureTrailingDot(target)
   323  		}
   324  
   325  		records = append(records, &domain.Record{
   326  			Data:     finalTargetName,
   327  			Name:     strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
   328  			Priority: priority,
   329  			TTL:      ttl,
   330  			Type:     domain.RecordType(ep.RecordType),
   331  		})
   332  	}
   333  
   334  	return records
   335  }
   336  
   337  func endpointToScalewayRecordsChangeDelete(zoneName string, ep *endpoint.Endpoint) []*domain.RecordChange {
   338  	records := []*domain.RecordChange{}
   339  
   340  	for _, target := range ep.Targets {
   341  		finalTargetName := target
   342  		if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
   343  			finalTargetName = provider.EnsureTrailingDot(target)
   344  		}
   345  
   346  		records = append(records, &domain.RecordChange{
   347  			Delete: &domain.RecordChangeDelete{
   348  				IDFields: &domain.RecordIdentifier{
   349  					Data: &finalTargetName,
   350  					Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
   351  					Type: domain.RecordType(ep.RecordType),
   352  				},
   353  			},
   354  		})
   355  	}
   356  
   357  	return records
   358  }
   359  
   360  func logChanges(req *domain.UpdateDNSZoneRecordsRequest) {
   361  	if !log.IsLevelEnabled(log.InfoLevel) {
   362  		return
   363  	}
   364  	log.Infof("Updating zone %s", req.DNSZone)
   365  	for _, change := range req.Changes {
   366  		if change.Add != nil {
   367  			for _, add := range change.Add.Records {
   368  				name := add.Name + "."
   369  				if add.Name == "" {
   370  					name = ""
   371  				}
   372  
   373  				logFields := log.Fields{
   374  					"record":   name + req.DNSZone,
   375  					"type":     add.Type.String(),
   376  					"ttl":      add.TTL,
   377  					"priority": add.Priority,
   378  					"data":     add.Data,
   379  				}
   380  				log.WithFields(logFields).Info("Adding record")
   381  			}
   382  		} else if change.Delete != nil {
   383  			name := change.Delete.IDFields.Name + "."
   384  			if change.Delete.IDFields.Name == "" {
   385  				name = ""
   386  			}
   387  
   388  			logFields := log.Fields{
   389  				"record": name + req.DNSZone,
   390  				"type":   change.Delete.IDFields.Type.String(),
   391  				"data":   *change.Delete.IDFields.Data,
   392  			}
   393  
   394  			log.WithFields(logFields).Info("Deleting record")
   395  		}
   396  	}
   397  }