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

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6      http://www.apache.org/licenses/LICENSE-2.0
     7  Unless required by applicable law or agreed to in writing, software
     8  distributed under the License is distributed on an "AS IS" BASIS,
     9  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    10  See the License for the specific language governing permissions and
    11  limitations under the License.
    12  */
    13  
    14  package gandi
    15  
    16  import (
    17  	"context"
    18  	"errors"
    19  	"os"
    20  	"strings"
    21  
    22  	"github.com/go-gandi/go-gandi"
    23  	"github.com/go-gandi/go-gandi/config"
    24  	"github.com/go-gandi/go-gandi/livedns"
    25  	log "github.com/sirupsen/logrus"
    26  
    27  	"sigs.k8s.io/external-dns/endpoint"
    28  	"sigs.k8s.io/external-dns/plan"
    29  	"sigs.k8s.io/external-dns/provider"
    30  )
    31  
    32  const (
    33  	gandiCreate          = "CREATE"
    34  	gandiDelete          = "DELETE"
    35  	gandiUpdate          = "UPDATE"
    36  	gandiTTL             = 600
    37  	gandiLiveDNSProvider = "livedns"
    38  )
    39  
    40  type GandiChanges struct {
    41  	Action   string
    42  	ZoneName string
    43  	Record   livedns.DomainRecord
    44  }
    45  
    46  type GandiProvider struct {
    47  	provider.BaseProvider
    48  	LiveDNSClient LiveDNSClientAdapter
    49  	DomainClient  DomainClientAdapter
    50  	domainFilter  endpoint.DomainFilter
    51  	DryRun        bool
    52  }
    53  
    54  func NewGandiProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*GandiProvider, error) {
    55  	key, ok_key := os.LookupEnv("GANDI_KEY")
    56  	pat, ok_pat := os.LookupEnv("GANDI_PAT")
    57  	if !(ok_key || ok_pat) {
    58  		return nil, errors.New("no environment variable GANDI_KEY or GANDI_PAT provided")
    59  	}
    60  	if ok_key {
    61  		log.Warning("Usage of GANDI_KEY (API Key) is deprecated. Please consider creating a Personal Access Token (PAT) instead, see https://api.gandi.net/docs/authentication/")
    62  	}
    63  	sharingID, _ := os.LookupEnv("GANDI_SHARING_ID")
    64  
    65  	g := config.Config{
    66  		APIKey:              key,
    67  		PersonalAccessToken: pat,
    68  		SharingID:           sharingID,
    69  		Debug:               false,
    70  		// dry-run doesn't work but it won't hurt passing the flag
    71  		DryRun: dryRun,
    72  	}
    73  
    74  	liveDNSClient := gandi.NewLiveDNSClient(g)
    75  	domainClient := gandi.NewDomainClient(g)
    76  
    77  	gandiProvider := &GandiProvider{
    78  		LiveDNSClient: NewLiveDNSClient(liveDNSClient),
    79  		DomainClient:  NewDomainClient(domainClient),
    80  		domainFilter:  domainFilter,
    81  		DryRun:        dryRun,
    82  	}
    83  	return gandiProvider, nil
    84  }
    85  
    86  func (p *GandiProvider) Zones() (zones []string, err error) {
    87  	availableDomains, err := p.DomainClient.ListDomains()
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  	zones = []string{}
    92  	for _, domain := range availableDomains {
    93  		if !p.domainFilter.Match(domain.FQDN) {
    94  			log.Debugf("Excluding domain %s by domain-filter", domain.FQDN)
    95  			continue
    96  		}
    97  
    98  		if domain.NameServer.Current != gandiLiveDNSProvider {
    99  			log.Debugf("Excluding domain %s, not configured for livedns", domain.FQDN)
   100  			continue
   101  		}
   102  
   103  		zones = append(zones, domain.FQDN)
   104  	}
   105  	return zones, nil
   106  }
   107  
   108  func (p *GandiProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
   109  	liveDNSZones, err := p.Zones()
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	endpoints := []*endpoint.Endpoint{}
   114  	for _, zone := range liveDNSZones {
   115  		records, err := p.LiveDNSClient.GetDomainRecords(zone)
   116  		if err != nil {
   117  			return nil, err
   118  		}
   119  
   120  		for _, r := range records {
   121  			if provider.SupportedRecordType(r.RrsetType) {
   122  				name := r.RrsetName + "." + zone
   123  
   124  				if r.RrsetName == "@" {
   125  					name = zone
   126  				}
   127  
   128  				for _, v := range r.RrsetValues {
   129  					log.WithFields(log.Fields{
   130  						"record": r.RrsetName,
   131  						"type":   r.RrsetType,
   132  						"value":  v,
   133  						"ttl":    r.RrsetTTL,
   134  						"zone":   zone,
   135  					}).Debug("Returning endpoint record")
   136  
   137  					endpoints = append(
   138  						endpoints,
   139  						endpoint.NewEndpointWithTTL(name, r.RrsetType, endpoint.TTL(r.RrsetTTL), v),
   140  					)
   141  				}
   142  			}
   143  		}
   144  	}
   145  
   146  	return endpoints, nil
   147  }
   148  
   149  func (p *GandiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
   150  	combinedChanges := make([]*GandiChanges, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
   151  
   152  	combinedChanges = append(combinedChanges, p.newGandiChanges(gandiCreate, changes.Create)...)
   153  	combinedChanges = append(combinedChanges, p.newGandiChanges(gandiUpdate, changes.UpdateNew)...)
   154  	combinedChanges = append(combinedChanges, p.newGandiChanges(gandiDelete, changes.Delete)...)
   155  
   156  	return p.submitChanges(ctx, combinedChanges)
   157  }
   158  
   159  func (p *GandiProvider) submitChanges(ctx context.Context, changes []*GandiChanges) error {
   160  	if len(changes) == 0 {
   161  		log.Infof("All records are already up to date")
   162  		return nil
   163  	}
   164  
   165  	liveDNSDomains, err := p.Zones()
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	zoneChanges := p.groupAndFilterByZone(liveDNSDomains, changes)
   171  
   172  	for _, changes := range zoneChanges {
   173  		for _, change := range changes {
   174  			if change.Record.RrsetType == endpoint.RecordTypeCNAME && !strings.HasSuffix(change.Record.RrsetValues[0], ".") {
   175  				change.Record.RrsetValues[0] += "."
   176  			}
   177  
   178  			// Prepare record name
   179  			if change.Record.RrsetName == change.ZoneName {
   180  				log.WithFields(log.Fields{
   181  					"record": change.Record.RrsetName,
   182  					"type":   change.Record.RrsetType,
   183  					"value":  change.Record.RrsetValues[0],
   184  					"ttl":    change.Record.RrsetTTL,
   185  					"action": change.Action,
   186  					"zone":   change.ZoneName,
   187  				}).Debugf("Converting record name: %s to apex domain (@)", change.Record.RrsetName)
   188  
   189  				change.Record.RrsetName = "@"
   190  			} else {
   191  				change.Record.RrsetName = strings.TrimSuffix(
   192  					change.Record.RrsetName,
   193  					"."+change.ZoneName,
   194  				)
   195  			}
   196  
   197  			log.WithFields(log.Fields{
   198  				"record": change.Record.RrsetName,
   199  				"type":   change.Record.RrsetType,
   200  				"value":  change.Record.RrsetValues[0],
   201  				"ttl":    change.Record.RrsetTTL,
   202  				"action": change.Action,
   203  				"zone":   change.ZoneName,
   204  			}).Info("Changing record")
   205  
   206  			if !p.DryRun {
   207  				switch change.Action {
   208  				case gandiCreate:
   209  					answer, err := p.LiveDNSClient.CreateDomainRecord(
   210  						change.ZoneName,
   211  						change.Record.RrsetName,
   212  						change.Record.RrsetType,
   213  						change.Record.RrsetTTL,
   214  						change.Record.RrsetValues,
   215  					)
   216  					if err != nil {
   217  						log.WithFields(log.Fields{
   218  							"Code":    answer.Code,
   219  							"Message": answer.Message,
   220  							"Cause":   answer.Cause,
   221  							"Errors":  answer.Errors,
   222  						}).Warning("Create problem")
   223  						return err
   224  					}
   225  				case gandiDelete:
   226  					err := p.LiveDNSClient.DeleteDomainRecord(change.ZoneName, change.Record.RrsetName, change.Record.RrsetType)
   227  					if err != nil {
   228  						log.Warning("Delete problem")
   229  						return err
   230  					}
   231  				case gandiUpdate:
   232  					answer, err := p.LiveDNSClient.UpdateDomainRecordByNameAndType(
   233  						change.ZoneName,
   234  						change.Record.RrsetName,
   235  						change.Record.RrsetType,
   236  						change.Record.RrsetTTL,
   237  						change.Record.RrsetValues,
   238  					)
   239  					if err != nil {
   240  						log.WithFields(log.Fields{
   241  							"Code":    answer.Code,
   242  							"Message": answer.Message,
   243  							"Cause":   answer.Cause,
   244  							"Errors":  answer.Errors,
   245  						}).Warning("Update problem")
   246  						return err
   247  					}
   248  				}
   249  			}
   250  		}
   251  	}
   252  
   253  	return nil
   254  }
   255  
   256  func (p *GandiProvider) newGandiChanges(action string, endpoints []*endpoint.Endpoint) []*GandiChanges {
   257  	changes := make([]*GandiChanges, 0, len(endpoints))
   258  	ttl := gandiTTL
   259  	for _, e := range endpoints {
   260  		if e.RecordTTL.IsConfigured() {
   261  			ttl = int(e.RecordTTL)
   262  		}
   263  		change := &GandiChanges{
   264  			Action: action,
   265  			Record: livedns.DomainRecord{
   266  				RrsetType:   e.RecordType,
   267  				RrsetName:   e.DNSName,
   268  				RrsetValues: e.Targets,
   269  				RrsetTTL:    ttl,
   270  			},
   271  		}
   272  		changes = append(changes, change)
   273  	}
   274  	return changes
   275  }
   276  
   277  func (p *GandiProvider) groupAndFilterByZone(zones []string, changes []*GandiChanges) map[string][]*GandiChanges {
   278  	change := make(map[string][]*GandiChanges)
   279  	zoneNameID := provider.ZoneIDName{}
   280  
   281  	for _, z := range zones {
   282  		zoneNameID.Add(z, z)
   283  		change[z] = []*GandiChanges{}
   284  	}
   285  
   286  	for _, c := range changes {
   287  		zoneID, zoneName := zoneNameID.FindZone(c.Record.RrsetName)
   288  		if zoneName == "" {
   289  			log.Debugf("Skipping record %s because no hosted domain matching record DNS Name was detected", c.Record.RrsetName)
   290  			continue
   291  		}
   292  		c.ZoneName = zoneName
   293  		change[zoneID] = append(change[zoneID], c)
   294  	}
   295  	return change
   296  }