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

     1  /*
     2  Copyright 2021 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 safedns
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  
    24  	ansClient "github.com/ans-group/sdk-go/pkg/client"
    25  	ansConnection "github.com/ans-group/sdk-go/pkg/connection"
    26  	"github.com/ans-group/sdk-go/pkg/service/safedns"
    27  	log "github.com/sirupsen/logrus"
    28  
    29  	"sigs.k8s.io/external-dns/endpoint"
    30  	"sigs.k8s.io/external-dns/plan"
    31  	"sigs.k8s.io/external-dns/provider"
    32  )
    33  
    34  // SafeDNS is an interface that is a subset of the SafeDNS service API that are actually used.
    35  // Signatures must match exactly.
    36  type SafeDNS interface {
    37  	CreateZoneRecord(zoneName string, req safedns.CreateRecordRequest) (int, error)
    38  	DeleteZoneRecord(zoneName string, recordID int) error
    39  	GetZone(zoneName string) (safedns.Zone, error)
    40  	GetZoneRecord(zoneName string, recordID int) (safedns.Record, error)
    41  	GetZoneRecords(zoneName string, parameters ansConnection.APIRequestParameters) ([]safedns.Record, error)
    42  	GetZones(parameters ansConnection.APIRequestParameters) ([]safedns.Zone, error)
    43  	PatchZoneRecord(zoneName string, recordID int, patch safedns.PatchRecordRequest) (int, error)
    44  	UpdateZoneRecord(zoneName string, record safedns.Record) (int, error)
    45  }
    46  
    47  // SafeDNSProvider implements the DNS provider spec for UKFast SafeDNS.
    48  type SafeDNSProvider struct {
    49  	provider.BaseProvider
    50  	Client SafeDNS
    51  	// Only consider hosted zones managing domains ending in this suffix
    52  	domainFilter     endpoint.DomainFilter
    53  	DryRun           bool
    54  	APIRequestParams ansConnection.APIRequestParameters
    55  }
    56  
    57  // ZoneRecord is a datatype to simplify management of a record in a zone.
    58  type ZoneRecord struct {
    59  	ID      int
    60  	Name    string
    61  	Type    safedns.RecordType
    62  	TTL     safedns.RecordTTL
    63  	Zone    string
    64  	Content string
    65  }
    66  
    67  func NewSafeDNSProvider(domainFilter endpoint.DomainFilter, dryRun bool) (*SafeDNSProvider, error) {
    68  	token, ok := os.LookupEnv("SAFEDNS_TOKEN")
    69  	if !ok {
    70  		return nil, fmt.Errorf("no SAFEDNS_TOKEN found in environment")
    71  	}
    72  
    73  	ukfAPIConnection := ansConnection.NewAPIKeyCredentialsAPIConnection(token)
    74  	ansClient := ansClient.NewClient(ukfAPIConnection)
    75  	safeDNS := ansClient.SafeDNSService()
    76  
    77  	provider := &SafeDNSProvider{
    78  		Client:           safeDNS,
    79  		domainFilter:     domainFilter,
    80  		DryRun:           dryRun,
    81  		APIRequestParams: *ansConnection.NewAPIRequestParameters(),
    82  	}
    83  	return provider, nil
    84  }
    85  
    86  // Zones returns the list of hosted zones in the SafeDNS account
    87  func (p *SafeDNSProvider) Zones(ctx context.Context) ([]safedns.Zone, error) {
    88  	var zones []safedns.Zone
    89  
    90  	allZones, err := p.Client.GetZones(p.APIRequestParams)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	// Check each found zone to see whether they match the domain filter provided. If they do, append it to the array of
    96  	// zones defined above. If not, continue to the next item in the loop.
    97  	for _, zone := range allZones {
    98  		if p.domainFilter.Match(zone.Name) {
    99  			zones = append(zones, zone)
   100  		} else {
   101  			continue
   102  		}
   103  	}
   104  	return zones, nil
   105  }
   106  
   107  func (p *SafeDNSProvider) ZoneRecords(ctx context.Context) ([]ZoneRecord, error) {
   108  	zones, err := p.Zones(ctx)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	var zoneRecords []ZoneRecord
   114  	for _, zone := range zones {
   115  		// For each zone in the zonelist, get all records of an ExternalDNS supported type.
   116  		records, err := p.Client.GetZoneRecords(zone.Name, p.APIRequestParams)
   117  		if err != nil {
   118  			return nil, err
   119  		}
   120  		for _, r := range records {
   121  			zoneRecord := ZoneRecord{
   122  				ID:      r.ID,
   123  				Name:    r.Name,
   124  				Type:    r.Type,
   125  				TTL:     r.TTL,
   126  				Zone:    zone.Name,
   127  				Content: r.Content,
   128  			}
   129  			zoneRecords = append(zoneRecords, zoneRecord)
   130  		}
   131  	}
   132  	return zoneRecords, nil
   133  }
   134  
   135  // Records returns a list of Endpoint resources created from all records in supported zones.
   136  func (p *SafeDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
   137  	var endpoints []*endpoint.Endpoint
   138  	zoneRecords, err := p.ZoneRecords(ctx)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	for _, r := range zoneRecords {
   143  		if provider.SupportedRecordType(string(r.Type)) {
   144  			endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, string(r.Type), endpoint.TTL(r.TTL), r.Content))
   145  		}
   146  	}
   147  	return endpoints, nil
   148  }
   149  
   150  // ApplyChanges applies a given set of changes in a given zone.
   151  func (p *SafeDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
   152  	// Identify the zone name for each record
   153  	zoneNameIDMapper := provider.ZoneIDName{}
   154  
   155  	zones, err := p.Zones(ctx)
   156  	if err != nil {
   157  		return err
   158  	}
   159  	for _, zone := range zones {
   160  		zoneNameIDMapper.Add(zone.Name, zone.Name)
   161  	}
   162  
   163  	zoneRecords, err := p.ZoneRecords(ctx)
   164  	if err != nil {
   165  		return err
   166  	}
   167  
   168  	for _, endpoint := range changes.Create {
   169  		_, ZoneName := zoneNameIDMapper.FindZone(endpoint.DNSName)
   170  		for _, target := range endpoint.Targets {
   171  			request := safedns.CreateRecordRequest{
   172  				Name:    endpoint.DNSName,
   173  				Type:    endpoint.RecordType,
   174  				Content: target,
   175  			}
   176  			log.WithFields(log.Fields{
   177  				"zoneID":     ZoneName,
   178  				"dnsName":    endpoint.DNSName,
   179  				"recordType": endpoint.RecordType,
   180  				"Value":      target,
   181  			}).Info("Creating record")
   182  			_, err := p.Client.CreateZoneRecord(ZoneName, request)
   183  			if err != nil {
   184  				return err
   185  			}
   186  		}
   187  	}
   188  	for _, endpoint := range changes.UpdateNew {
   189  		// Currently iterates over each zoneRecord in ZoneRecords for each Endpoint
   190  		// in UpdateNew; the same will go for Delete. As it's double-iteration,
   191  		// that's O(n^2), which isn't great. No performance issues have been noted
   192  		// thus far.
   193  		var zoneRecord ZoneRecord
   194  		for _, target := range endpoint.Targets {
   195  			for _, zr := range zoneRecords {
   196  				if zr.Name == endpoint.DNSName && zr.Content == target {
   197  					zoneRecord = zr
   198  					break
   199  				}
   200  			}
   201  
   202  			newTTL := safedns.RecordTTL(int(endpoint.RecordTTL))
   203  			newRecord := safedns.PatchRecordRequest{
   204  				Name:    endpoint.DNSName,
   205  				Content: target,
   206  				TTL:     &newTTL,
   207  				Type:    endpoint.RecordType,
   208  			}
   209  			log.WithFields(log.Fields{
   210  				"zoneID":     zoneRecord.Zone,
   211  				"dnsName":    newRecord.Name,
   212  				"recordType": newRecord.Type,
   213  				"Value":      newRecord.Content,
   214  				"Priority":   newRecord.Priority,
   215  			}).Info("Patching record")
   216  			_, err = p.Client.PatchZoneRecord(zoneRecord.Zone, zoneRecord.ID, newRecord)
   217  			if err != nil {
   218  				return err
   219  			}
   220  		}
   221  	}
   222  	for _, endpoint := range changes.Delete {
   223  		// As above, currently iterates in O(n^2). May be a good start for optimisations.
   224  		var zoneRecord ZoneRecord
   225  		for _, zr := range zoneRecords {
   226  			if zr.Name == endpoint.DNSName && string(zr.Type) == endpoint.RecordType {
   227  				zoneRecord = zr
   228  				break
   229  			}
   230  		}
   231  		log.WithFields(log.Fields{
   232  			"zoneID":     zoneRecord.Zone,
   233  			"dnsName":    zoneRecord.Name,
   234  			"recordType": zoneRecord.Type,
   235  		}).Info("Deleting record")
   236  		err := p.Client.DeleteZoneRecord(zoneRecord.Zone, zoneRecord.ID)
   237  		if err != nil {
   238  			return err
   239  		}
   240  	}
   241  	return nil
   242  }