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 }