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 }