sigs.k8s.io/external-dns@v0.14.1/provider/ibmcloud/ibmcloud.go (about) 1 /* 2 Copyright 2022 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 ibmcloud 18 19 import ( 20 "context" 21 "fmt" 22 "os" 23 "reflect" 24 "strconv" 25 "strings" 26 27 "github.com/IBM-Cloud/ibm-cloud-cli-sdk/bluemix/crn" 28 "github.com/IBM/go-sdk-core/v5/core" 29 "github.com/IBM/networking-go-sdk/dnsrecordsv1" 30 "github.com/IBM/networking-go-sdk/dnssvcsv1" 31 "github.com/IBM/networking-go-sdk/zonesv1" 32 "gopkg.in/yaml.v2" 33 34 log "github.com/sirupsen/logrus" 35 36 "sigs.k8s.io/external-dns/endpoint" 37 "sigs.k8s.io/external-dns/plan" 38 "sigs.k8s.io/external-dns/provider" 39 "sigs.k8s.io/external-dns/source" 40 ) 41 42 var proxyTypeNotSupported = map[string]bool{ 43 "LOC": true, 44 "MX": true, 45 "NS": true, 46 "SPF": true, 47 "TXT": true, 48 "SRV": true, 49 } 50 51 var privateTypeSupported = map[string]bool{ 52 "A": true, 53 "CNAME": true, 54 "TXT": true, 55 } 56 57 const ( 58 // recordCreate is a ChangeAction enum value 59 recordCreate = "CREATE" 60 // recordDelete is a ChangeAction enum value 61 recordDelete = "DELETE" 62 // recordUpdate is a ChangeAction enum value 63 recordUpdate = "UPDATE" 64 // defaultPublicRecordTTL 1 = automatic 65 defaultPublicRecordTTL = 1 66 67 proxyFilter = "ibmcloud-proxied" 68 vpcFilter = "ibmcloud-vpc" 69 zoneStatePendingNetwork = "PENDING_NETWORK_ADD" 70 zoneStateActive = "ACTIVE" 71 ) 72 73 // Source shadow the interface source.Source. used primarily for unit testing. 74 type Source interface { 75 Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) 76 AddEventHandler(context.Context, func()) 77 } 78 79 // ibmcloudClient is a minimal implementation of DNS API that we actually use, used primarily for unit testing. 80 type ibmcloudClient interface { 81 ListAllDDNSRecordsWithContext(ctx context.Context, listAllDNSRecordsOptions *dnsrecordsv1.ListAllDnsRecordsOptions) (result *dnsrecordsv1.ListDnsrecordsResp, response *core.DetailedResponse, err error) 82 CreateDNSRecordWithContext(ctx context.Context, createDNSRecordOptions *dnsrecordsv1.CreateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) 83 DeleteDNSRecordWithContext(ctx context.Context, deleteDNSRecordOptions *dnsrecordsv1.DeleteDnsRecordOptions) (result *dnsrecordsv1.DeleteDnsrecordResp, response *core.DetailedResponse, err error) 84 UpdateDNSRecordWithContext(ctx context.Context, updateDNSRecordOptions *dnsrecordsv1.UpdateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) 85 ListDnszonesWithContext(ctx context.Context, listDnszonesOptions *dnssvcsv1.ListDnszonesOptions) (result *dnssvcsv1.ListDnszones, response *core.DetailedResponse, err error) 86 GetDnszoneWithContext(ctx context.Context, getDnszoneOptions *dnssvcsv1.GetDnszoneOptions) (result *dnssvcsv1.Dnszone, response *core.DetailedResponse, err error) 87 CreatePermittedNetworkWithContext(ctx context.Context, createPermittedNetworkOptions *dnssvcsv1.CreatePermittedNetworkOptions) (result *dnssvcsv1.PermittedNetwork, response *core.DetailedResponse, err error) 88 ListResourceRecordsWithContext(ctx context.Context, listResourceRecordsOptions *dnssvcsv1.ListResourceRecordsOptions) (result *dnssvcsv1.ListResourceRecords, response *core.DetailedResponse, err error) 89 CreateResourceRecordWithContext(ctx context.Context, createResourceRecordOptions *dnssvcsv1.CreateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) 90 DeleteResourceRecordWithContext(ctx context.Context, deleteResourceRecordOptions *dnssvcsv1.DeleteResourceRecordOptions) (response *core.DetailedResponse, err error) 91 UpdateResourceRecordWithContext(ctx context.Context, updateResourceRecordOptions *dnssvcsv1.UpdateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) 92 NewResourceRecordInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordInputRdataRdataARecord, err error) 93 NewResourceRecordInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord, err error) 94 NewResourceRecordInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord, err error) 95 NewResourceRecordUpdateInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord, err error) 96 NewResourceRecordUpdateInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord, err error) 97 NewResourceRecordUpdateInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord, err error) 98 } 99 100 type ibmcloudService struct { 101 publicZonesService *zonesv1.ZonesV1 102 publicRecordsService *dnsrecordsv1.DnsRecordsV1 103 privateDNSService *dnssvcsv1.DnsSvcsV1 104 } 105 106 func (i ibmcloudService) ListAllDDNSRecordsWithContext(ctx context.Context, listAllDNSRecordsOptions *dnsrecordsv1.ListAllDnsRecordsOptions) (result *dnsrecordsv1.ListDnsrecordsResp, response *core.DetailedResponse, err error) { 107 return i.publicRecordsService.ListAllDnsRecordsWithContext(ctx, listAllDNSRecordsOptions) 108 } 109 110 func (i ibmcloudService) CreateDNSRecordWithContext(ctx context.Context, createDNSRecordOptions *dnsrecordsv1.CreateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) { 111 return i.publicRecordsService.CreateDnsRecordWithContext(ctx, createDNSRecordOptions) 112 } 113 114 func (i ibmcloudService) DeleteDNSRecordWithContext(ctx context.Context, deleteDNSRecordOptions *dnsrecordsv1.DeleteDnsRecordOptions) (result *dnsrecordsv1.DeleteDnsrecordResp, response *core.DetailedResponse, err error) { 115 return i.publicRecordsService.DeleteDnsRecordWithContext(ctx, deleteDNSRecordOptions) 116 } 117 118 func (i ibmcloudService) UpdateDNSRecordWithContext(ctx context.Context, updateDNSRecordOptions *dnsrecordsv1.UpdateDnsRecordOptions) (result *dnsrecordsv1.DnsrecordResp, response *core.DetailedResponse, err error) { 119 return i.publicRecordsService.UpdateDnsRecordWithContext(ctx, updateDNSRecordOptions) 120 } 121 122 func (i ibmcloudService) ListDnszonesWithContext(ctx context.Context, listDnszonesOptions *dnssvcsv1.ListDnszonesOptions) (result *dnssvcsv1.ListDnszones, response *core.DetailedResponse, err error) { 123 return i.privateDNSService.ListDnszonesWithContext(ctx, listDnszonesOptions) 124 } 125 126 func (i ibmcloudService) GetDnszoneWithContext(ctx context.Context, getDnszoneOptions *dnssvcsv1.GetDnszoneOptions) (result *dnssvcsv1.Dnszone, response *core.DetailedResponse, err error) { 127 return i.privateDNSService.GetDnszoneWithContext(ctx, getDnszoneOptions) 128 } 129 130 func (i ibmcloudService) CreatePermittedNetworkWithContext(ctx context.Context, createPermittedNetworkOptions *dnssvcsv1.CreatePermittedNetworkOptions) (result *dnssvcsv1.PermittedNetwork, response *core.DetailedResponse, err error) { 131 return i.privateDNSService.CreatePermittedNetworkWithContext(ctx, createPermittedNetworkOptions) 132 } 133 134 func (i ibmcloudService) ListResourceRecordsWithContext(ctx context.Context, listResourceRecordsOptions *dnssvcsv1.ListResourceRecordsOptions) (result *dnssvcsv1.ListResourceRecords, response *core.DetailedResponse, err error) { 135 return i.privateDNSService.ListResourceRecordsWithContext(ctx, listResourceRecordsOptions) 136 } 137 138 func (i ibmcloudService) CreateResourceRecordWithContext(ctx context.Context, createResourceRecordOptions *dnssvcsv1.CreateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) { 139 return i.privateDNSService.CreateResourceRecordWithContext(ctx, createResourceRecordOptions) 140 } 141 142 func (i ibmcloudService) DeleteResourceRecordWithContext(ctx context.Context, deleteResourceRecordOptions *dnssvcsv1.DeleteResourceRecordOptions) (response *core.DetailedResponse, err error) { 143 return i.privateDNSService.DeleteResourceRecordWithContext(ctx, deleteResourceRecordOptions) 144 } 145 146 func (i ibmcloudService) UpdateResourceRecordWithContext(ctx context.Context, updateResourceRecordOptions *dnssvcsv1.UpdateResourceRecordOptions) (result *dnssvcsv1.ResourceRecord, response *core.DetailedResponse, err error) { 147 return i.privateDNSService.UpdateResourceRecordWithContext(ctx, updateResourceRecordOptions) 148 } 149 150 func (i ibmcloudService) NewResourceRecordInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordInputRdataRdataARecord, err error) { 151 return i.privateDNSService.NewResourceRecordInputRdataRdataARecord(ip) 152 } 153 154 func (i ibmcloudService) NewResourceRecordInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord, err error) { 155 return i.privateDNSService.NewResourceRecordInputRdataRdataCnameRecord(cname) 156 } 157 158 func (i ibmcloudService) NewResourceRecordInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord, err error) { 159 return i.privateDNSService.NewResourceRecordInputRdataRdataTxtRecord(text) 160 } 161 162 func (i ibmcloudService) NewResourceRecordUpdateInputRdataRdataARecord(ip string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataARecord, err error) { 163 return i.privateDNSService.NewResourceRecordUpdateInputRdataRdataARecord(ip) 164 } 165 166 func (i ibmcloudService) NewResourceRecordUpdateInputRdataRdataCnameRecord(cname string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataCnameRecord, err error) { 167 return i.privateDNSService.NewResourceRecordUpdateInputRdataRdataCnameRecord(cname) 168 } 169 170 func (i ibmcloudService) NewResourceRecordUpdateInputRdataRdataTxtRecord(text string) (model *dnssvcsv1.ResourceRecordUpdateInputRdataRdataTxtRecord, err error) { 171 return i.privateDNSService.NewResourceRecordUpdateInputRdataRdataTxtRecord(text) 172 } 173 174 // IBMCloudProvider is an implementation of Provider for IBM Cloud DNS. 175 type IBMCloudProvider struct { 176 provider.BaseProvider 177 source Source 178 Client ibmcloudClient 179 // only consider hosted zones managing domains ending in this suffix 180 domainFilter endpoint.DomainFilter 181 zoneIDFilter provider.ZoneIDFilter 182 instanceID string 183 privateZone bool 184 proxiedByDefault bool 185 DryRun bool 186 } 187 188 type ibmcloudConfig struct { 189 Endpoint string `json:"endpoint" yaml:"endpoint"` 190 APIKey string `json:"apiKey" yaml:"apiKey"` 191 CRN string `json:"instanceCrn" yaml:"instanceCrn"` 192 IAMURL string `json:"iamUrl" yaml:"iamUrl"` 193 InstanceID string `json:"-" yaml:"-"` 194 } 195 196 // ibmcloudChange differentiates between ChangActions 197 type ibmcloudChange struct { 198 Action string 199 PublicResourceRecord dnsrecordsv1.DnsrecordDetails 200 PrivateResourceRecord dnssvcsv1.ResourceRecord 201 } 202 203 func getConfig(configFile string) (*ibmcloudConfig, error) { 204 contents, err := os.ReadFile(configFile) 205 if err != nil { 206 return nil, fmt.Errorf("failed to read IBM Cloud config file '%s': %v", configFile, err) 207 } 208 cfg := &ibmcloudConfig{} 209 err = yaml.Unmarshal(contents, &cfg) 210 if err != nil { 211 return nil, fmt.Errorf("failed to read IBM Cloud config file '%s': %v", configFile, err) 212 } 213 214 return cfg, nil 215 } 216 217 func (c *ibmcloudConfig) Validate(authenticator core.Authenticator, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter) (ibmcloudService, bool, error) { 218 var service ibmcloudService 219 isPrivate := false 220 log.Debugf("filters: %v, %v", domainFilter.Filters, zoneIDFilter.ZoneIDs) 221 if (len(domainFilter.Filters) == 0 || domainFilter.Filters[0] == "") && zoneIDFilter.ZoneIDs[0] == "" { 222 return service, isPrivate, fmt.Errorf("at lease one of filters: 'domain-filter', 'zone-id-filter' needed") 223 } 224 225 crn, err := crn.Parse(c.CRN) 226 if err != nil { 227 return service, isPrivate, err 228 } 229 log.Infof("IBM Cloud Service: %s", crn.ServiceName) 230 c.InstanceID = crn.ServiceInstance 231 232 switch { 233 case strings.Contains(crn.ServiceName, "internet-svcs"): 234 if len(domainFilter.Filters) > 1 || len(zoneIDFilter.ZoneIDs) > 1 { 235 return service, isPrivate, fmt.Errorf("for public zone, only one domain id filter or domain name filter allowed") 236 } 237 var zoneID string 238 // Public DNS service 239 service.publicZonesService, err = zonesv1.NewZonesV1(&zonesv1.ZonesV1Options{ 240 Authenticator: authenticator, 241 Crn: core.StringPtr(c.CRN), 242 }) 243 if err != nil { 244 return service, isPrivate, fmt.Errorf("failed to initialize ibmcloud public zones client: %v", err) 245 } 246 if c.Endpoint != "" { 247 service.publicZonesService.SetServiceURL(c.Endpoint) 248 } 249 250 zonesResp, _, err := service.publicZonesService.ListZones(&zonesv1.ListZonesOptions{}) 251 if err != nil { 252 return service, isPrivate, fmt.Errorf("failed to list ibmcloud public zones: %v", err) 253 } 254 for _, zone := range zonesResp.Result { 255 log.Debugf("zoneName: %s, zoneID: %s", *zone.Name, *zone.ID) 256 if len(domainFilter.Filters) > 0 && domainFilter.Filters[0] != "" && domainFilter.Match(*zone.Name) { 257 log.Debugf("zone %s found.", *zone.ID) 258 zoneID = *zone.ID 259 break 260 } 261 if len(zoneIDFilter.ZoneIDs[0]) != 0 && zoneIDFilter.Match(*zone.ID) { 262 log.Debugf("zone %s found.", *zone.ID) 263 zoneID = *zone.ID 264 break 265 } 266 } 267 if len(zoneID) == 0 { 268 return service, isPrivate, fmt.Errorf("no matched zone found") 269 } 270 271 service.publicRecordsService, err = dnsrecordsv1.NewDnsRecordsV1(&dnsrecordsv1.DnsRecordsV1Options{ 272 Authenticator: authenticator, 273 Crn: core.StringPtr(c.CRN), 274 ZoneIdentifier: core.StringPtr(zoneID), 275 }) 276 if err != nil { 277 return service, isPrivate, fmt.Errorf("failed to initialize ibmcloud public records client: %v", err) 278 } 279 if c.Endpoint != "" { 280 service.publicRecordsService.SetServiceURL(c.Endpoint) 281 } 282 case strings.Contains(crn.ServiceName, "dns-svcs"): 283 isPrivate = true 284 // Private DNS service 285 service.privateDNSService, err = dnssvcsv1.NewDnsSvcsV1(&dnssvcsv1.DnsSvcsV1Options{ 286 Authenticator: authenticator, 287 }) 288 if err != nil { 289 return service, isPrivate, fmt.Errorf("failed to initialize ibmcloud private records client: %v", err) 290 } 291 if c.Endpoint != "" { 292 service.privateDNSService.SetServiceURL(c.Endpoint) 293 } 294 default: 295 return service, isPrivate, fmt.Errorf("IBM Cloud instance crn is not provided or invalid dns crn : %s", c.CRN) 296 } 297 298 return service, isPrivate, nil 299 } 300 301 // NewIBMCloudProvider creates a new IBMCloud provider. 302 // 303 // Returns the provider or an error if a provider could not be created. 304 func NewIBMCloudProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, source source.Source, proxiedByDefault bool, dryRun bool) (*IBMCloudProvider, error) { 305 cfg, err := getConfig(configFile) 306 if err != nil { 307 return nil, err 308 } 309 310 authenticator := &core.IamAuthenticator{ 311 ApiKey: cfg.APIKey, 312 } 313 if cfg.IAMURL != "" { 314 authenticator = &core.IamAuthenticator{ 315 ApiKey: cfg.APIKey, 316 URL: cfg.IAMURL, 317 } 318 } 319 320 client, isPrivate, err := cfg.Validate(authenticator, domainFilter, zoneIDFilter) 321 if err != nil { 322 return nil, err 323 } 324 325 provider := &IBMCloudProvider{ 326 Client: client, 327 source: source, 328 domainFilter: domainFilter, 329 zoneIDFilter: zoneIDFilter, 330 instanceID: cfg.InstanceID, 331 privateZone: isPrivate, 332 proxiedByDefault: proxiedByDefault, 333 DryRun: dryRun, 334 } 335 return provider, nil 336 } 337 338 // Records gets the current records. 339 // 340 // Returns the current records or an error if the operation failed. 341 func (p *IBMCloudProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) { 342 if p.privateZone { 343 endpoints, err = p.privateRecords(ctx) 344 } else { 345 endpoints, err = p.publicRecords(ctx) 346 } 347 return endpoints, err 348 } 349 350 // ApplyChanges applies a given set of changes in a given zone. 351 func (p *IBMCloudProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 352 log.Debugln("applying change...") 353 ibmcloudChanges := []*ibmcloudChange{} 354 for _, endpoint := range changes.Create { 355 for _, target := range endpoint.Targets { 356 ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordCreate, endpoint, target)) 357 } 358 } 359 360 for i, desired := range changes.UpdateNew { 361 current := changes.UpdateOld[i] 362 363 add, remove, leave := provider.Difference(current.Targets, desired.Targets) 364 365 log.Debugf("add: %v, remove: %v, leave: %v", add, remove, leave) 366 for _, a := range add { 367 ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordCreate, desired, a)) 368 } 369 370 for _, a := range leave { 371 ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordUpdate, desired, a)) 372 } 373 374 for _, a := range remove { 375 ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordDelete, current, a)) 376 } 377 } 378 379 for _, endpoint := range changes.Delete { 380 for _, target := range endpoint.Targets { 381 ibmcloudChanges = append(ibmcloudChanges, p.newIBMCloudChange(recordDelete, endpoint, target)) 382 } 383 } 384 385 return p.submitChanges(ctx, ibmcloudChanges) 386 } 387 388 // AdjustEndpoints modifies the endpoints as needed by the specific provider 389 func (p *IBMCloudProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) { 390 adjustedEndpoints := []*endpoint.Endpoint{} 391 for _, e := range endpoints { 392 log.Debugf("adjusting endpont: %v", *e) 393 proxied := shouldBeProxied(e, p.proxiedByDefault) 394 if proxied { 395 e.RecordTTL = 0 396 } 397 e.SetProviderSpecificProperty(proxyFilter, strconv.FormatBool(proxied)) 398 399 adjustedEndpoints = append(adjustedEndpoints, e) 400 } 401 return adjustedEndpoints, nil 402 } 403 404 // submitChanges takes a zone and a collection of Changes and sends them as a single transaction. 405 func (p *IBMCloudProvider) submitChanges(ctx context.Context, changes []*ibmcloudChange) error { 406 // return early if there is nothing to change 407 if len(changes) == 0 { 408 return nil 409 } 410 411 log.Debugln("submmiting change...") 412 if p.privateZone { 413 return p.submitChangesForPrivateDNS(ctx, changes) 414 } 415 return p.submitChangesForPublicDNS(ctx, changes) 416 } 417 418 // submitChangesForPublicDNS takes a zone and a collection of Changes and sends them as a single transaction on public dns. 419 func (p *IBMCloudProvider) submitChangesForPublicDNS(ctx context.Context, changes []*ibmcloudChange) error { 420 records, err := p.listAllPublicRecords(ctx) 421 if err != nil { 422 return err 423 } 424 425 for _, change := range changes { 426 logFields := log.Fields{ 427 "record": *change.PublicResourceRecord.Name, 428 "type": *change.PublicResourceRecord.Type, 429 "ttl": *change.PublicResourceRecord.TTL, 430 "action": change.Action, 431 } 432 433 if p.DryRun { 434 continue 435 } 436 437 log.WithFields(logFields).Info("Changing record.") 438 439 if change.Action == recordUpdate { 440 recordID := p.getPublicRecordID(records, change.PublicResourceRecord) 441 if recordID == "" { 442 log.WithFields(logFields).Errorf("failed to find previous record: %v", *change.PublicResourceRecord.Name) 443 continue 444 } 445 p.updateRecord(ctx, "", recordID, change) 446 } else if change.Action == recordDelete { 447 recordID := p.getPublicRecordID(records, change.PublicResourceRecord) 448 if recordID == "" { 449 log.WithFields(logFields).Errorf("failed to find previous record: %v", *change.PublicResourceRecord.Name) 450 continue 451 } 452 p.deleteRecord(ctx, "", recordID) 453 } else if change.Action == recordCreate { 454 p.createRecord(ctx, "", change) 455 } 456 } 457 458 return nil 459 } 460 461 // submitChangesForPrivateDNS takes a zone and a collection of Changes and sends them as a single transaction on private dns. 462 func (p *IBMCloudProvider) submitChangesForPrivateDNS(ctx context.Context, changes []*ibmcloudChange) error { 463 zones, err := p.privateZones(ctx) 464 if err != nil { 465 return err 466 } 467 // separate into per-zone change sets to be passed to the API. 468 changesByPrivateZone := p.changesByPrivateZone(ctx, zones, changes) 469 470 for zoneID, changes := range changesByPrivateZone { 471 records, err := p.listAllPrivateRecords(ctx, zoneID) 472 if err != nil { 473 return err 474 } 475 476 for _, change := range changes { 477 logFields := log.Fields{ 478 "record": *change.PrivateResourceRecord.Name, 479 "type": *change.PrivateResourceRecord.Type, 480 "ttl": *change.PrivateResourceRecord.TTL, 481 "action": change.Action, 482 } 483 484 log.WithFields(logFields).Info("Changing record.") 485 486 if p.DryRun { 487 continue 488 } 489 490 if change.Action == recordUpdate { 491 recordID := p.getPrivateRecordID(records, change.PrivateResourceRecord) 492 if recordID == "" { 493 log.WithFields(logFields).Errorf("failed to find previous record: %v", change.PrivateResourceRecord) 494 continue 495 } 496 p.updateRecord(ctx, zoneID, recordID, change) 497 } else if change.Action == recordDelete { 498 recordID := p.getPrivateRecordID(records, change.PrivateResourceRecord) 499 if recordID == "" { 500 log.WithFields(logFields).Errorf("failed to find previous record: %v", change.PrivateResourceRecord) 501 continue 502 } 503 p.deleteRecord(ctx, zoneID, recordID) 504 } else if change.Action == recordCreate { 505 p.createRecord(ctx, zoneID, change) 506 } 507 } 508 } 509 510 return nil 511 } 512 513 // privateZones return zones in private dns 514 func (p *IBMCloudProvider) privateZones(ctx context.Context) ([]dnssvcsv1.Dnszone, error) { 515 result := []dnssvcsv1.Dnszone{} 516 // if there is a zoneIDfilter configured 517 // && if the filter isn't just a blank string (used in tests) 518 if len(p.zoneIDFilter.ZoneIDs) > 0 && p.zoneIDFilter.ZoneIDs[0] != "" { 519 log.Debugln("zoneIDFilter configured. only looking up zone IDs defined") 520 for _, zoneID := range p.zoneIDFilter.ZoneIDs { 521 log.Debugf("looking up zone %s", zoneID) 522 detailResponse, _, err := p.Client.GetDnszoneWithContext(ctx, &dnssvcsv1.GetDnszoneOptions{ 523 InstanceID: core.StringPtr(p.instanceID), 524 DnszoneID: core.StringPtr(zoneID), 525 }) 526 if err != nil { 527 log.Errorf("zone %s lookup failed, %v", zoneID, err) 528 continue 529 } 530 log.WithFields(log.Fields{ 531 "zoneName": *detailResponse.Name, 532 "zoneID": *detailResponse.ID, 533 }).Debugln("adding zone for consideration") 534 result = append(result, *detailResponse) 535 } 536 return result, nil 537 } 538 539 log.Debugln("no zoneIDFilter configured, looking at all zones") 540 541 zonesResponse, _, err := p.Client.ListDnszonesWithContext(ctx, &dnssvcsv1.ListDnszonesOptions{ 542 InstanceID: core.StringPtr(p.instanceID), 543 }) 544 if err != nil { 545 return nil, err 546 } 547 548 for _, zone := range zonesResponse.Dnszones { 549 if !p.domainFilter.Match(*zone.Name) { 550 log.Debugf("zone %s not in domain filter", *zone.Name) 551 continue 552 } 553 result = append(result, zone) 554 } 555 556 return result, nil 557 } 558 559 // activePrivateZone active zone with new records add if not active 560 func (p *IBMCloudProvider) activePrivateZone(ctx context.Context, zoneID, vpc string) { 561 permittedNetworkVpc := &dnssvcsv1.PermittedNetworkVpc{ 562 VpcCrn: core.StringPtr(vpc), 563 } 564 createPermittedNetworkOptions := &dnssvcsv1.CreatePermittedNetworkOptions{ 565 InstanceID: core.StringPtr(p.instanceID), 566 DnszoneID: core.StringPtr(zoneID), 567 PermittedNetwork: permittedNetworkVpc, 568 Type: core.StringPtr("vpc"), 569 } 570 _, _, err := p.Client.CreatePermittedNetworkWithContext(ctx, createPermittedNetworkOptions) 571 if err != nil { 572 log.Errorf("failed to active zone %s in VPC %s with error: %v", zoneID, vpc, err) 573 } 574 } 575 576 // changesByPrivateZone separates a multi-zone change into a single change per zone. 577 func (p *IBMCloudProvider) changesByPrivateZone(ctx context.Context, zones []dnssvcsv1.Dnszone, changeSet []*ibmcloudChange) map[string][]*ibmcloudChange { 578 changes := make(map[string][]*ibmcloudChange) 579 zoneNameIDMapper := provider.ZoneIDName{} 580 for _, z := range zones { 581 zoneNameIDMapper.Add(*z.ID, *z.Name) 582 changes[*z.ID] = []*ibmcloudChange{} 583 } 584 585 for _, c := range changeSet { 586 zoneID, _ := zoneNameIDMapper.FindZone(*c.PrivateResourceRecord.Name) 587 if zoneID == "" { 588 log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", *c.PrivateResourceRecord.Name) 589 continue 590 } 591 changes[zoneID] = append(changes[zoneID], c) 592 } 593 594 return changes 595 } 596 597 func (p *IBMCloudProvider) publicRecords(ctx context.Context) ([]*endpoint.Endpoint, error) { 598 log.Debugf("Listing records on public zone") 599 dnsRecords, err := p.listAllPublicRecords(ctx) 600 if err != nil { 601 return nil, err 602 } 603 return p.groupPublicRecords(dnsRecords), nil 604 } 605 606 func (p *IBMCloudProvider) listAllPublicRecords(ctx context.Context) ([]dnsrecordsv1.DnsrecordDetails, error) { 607 var dnsRecords []dnsrecordsv1.DnsrecordDetails 608 page := 1 609 GETRECORDS: 610 listAllDNSRecordsOptions := &dnsrecordsv1.ListAllDnsRecordsOptions{ 611 Page: core.Int64Ptr(int64(page)), 612 } 613 records, _, err := p.Client.ListAllDDNSRecordsWithContext(ctx, listAllDNSRecordsOptions) 614 if err != nil { 615 return dnsRecords, err 616 } 617 dnsRecords = append(dnsRecords, records.Result...) 618 // Loop if more records exist 619 if *records.ResultInfo.TotalCount > int64(page*100) { 620 page = page + 1 621 log.Debugf("More than one pages records found, page: %d", page) 622 goto GETRECORDS 623 } 624 return dnsRecords, nil 625 } 626 627 func (p *IBMCloudProvider) groupPublicRecords(records []dnsrecordsv1.DnsrecordDetails) []*endpoint.Endpoint { 628 endpoints := []*endpoint.Endpoint{} 629 630 // group supported records by name and type 631 groups := map[string][]dnsrecordsv1.DnsrecordDetails{} 632 633 for _, r := range records { 634 if !provider.SupportedRecordType(*r.Type) { 635 continue 636 } 637 638 groupBy := *r.Name + *r.Type 639 if _, ok := groups[groupBy]; !ok { 640 groups[groupBy] = []dnsrecordsv1.DnsrecordDetails{} 641 } 642 643 groups[groupBy] = append(groups[groupBy], r) 644 } 645 646 // create single endpoint with all the targets for each name/type 647 for _, records := range groups { 648 targets := make([]string, len(records)) 649 for i, record := range records { 650 targets[i] = *record.Content 651 } 652 653 ep := endpoint.NewEndpointWithTTL( 654 *records[0].Name, 655 *records[0].Type, 656 endpoint.TTL(*records[0].TTL), 657 targets...).WithProviderSpecific(proxyFilter, strconv.FormatBool(*records[0].Proxied)) 658 659 log.Debugf( 660 "Found %s record for '%s' with target '%s'.", 661 ep.RecordType, 662 ep.DNSName, 663 ep.Targets, 664 ) 665 666 endpoints = append(endpoints, ep) 667 } 668 return endpoints 669 } 670 671 func (p *IBMCloudProvider) privateRecords(ctx context.Context) ([]*endpoint.Endpoint, error) { 672 log.Debugf("Listing records on private zone") 673 var vpc string 674 zones, err := p.privateZones(ctx) 675 if err != nil { 676 return nil, err 677 } 678 sources, err := p.source.Endpoints(ctx) 679 if err != nil { 680 return nil, err 681 } 682 // Filter VPC annoation for private zone active 683 for _, source := range sources { 684 vpc = checkVPCAnnotation(source) 685 if len(vpc) > 0 { 686 log.Debugf("VPC found: %s", vpc) 687 break 688 } 689 } 690 691 endpoints := []*endpoint.Endpoint{} 692 for _, zone := range zones { 693 if len(vpc) > 0 && *zone.State == zoneStatePendingNetwork { 694 log.Debugf("active zone: %s", *zone.ID) 695 p.activePrivateZone(ctx, *zone.ID, vpc) 696 } 697 698 dnsRecords, err := p.listAllPrivateRecords(ctx, *zone.ID) 699 if err != nil { 700 return nil, err 701 } 702 endpoints = append(endpoints, p.groupPrivateRecords(dnsRecords)...) 703 } 704 705 return endpoints, nil 706 } 707 708 func (p *IBMCloudProvider) listAllPrivateRecords(ctx context.Context, zoneID string) ([]dnssvcsv1.ResourceRecord, error) { 709 var dnsRecords []dnssvcsv1.ResourceRecord 710 offset := 0 711 GETRECORDS: 712 listResourceRecordsOptions := &dnssvcsv1.ListResourceRecordsOptions{ 713 InstanceID: core.StringPtr(p.instanceID), 714 DnszoneID: core.StringPtr(zoneID), 715 Offset: core.Int64Ptr(int64(offset)), 716 } 717 records, _, err := p.Client.ListResourceRecordsWithContext(ctx, listResourceRecordsOptions) 718 if err != nil { 719 return dnsRecords, err 720 } 721 oRecords := records.ResourceRecords 722 dnsRecords = append(dnsRecords, oRecords...) 723 // Loop if more records exist 724 if int64(offset+1) < *records.TotalCount && int64(offset+200) < *records.TotalCount { 725 offset = offset + 200 726 log.Debugf("More than one pages records found, page: %d", offset/200+1) 727 goto GETRECORDS 728 } 729 return dnsRecords, nil 730 } 731 732 func (p *IBMCloudProvider) groupPrivateRecords(records []dnssvcsv1.ResourceRecord) []*endpoint.Endpoint { 733 endpoints := []*endpoint.Endpoint{} 734 // group supported records by name and type 735 groups := map[string][]dnssvcsv1.ResourceRecord{} 736 for _, r := range records { 737 if !provider.SupportedRecordType(*r.Type) || !privateTypeSupported[*r.Type] { 738 continue 739 } 740 rname := *r.Name 741 rtype := *r.Type 742 groupBy := rname + rtype 743 if _, ok := groups[groupBy]; !ok { 744 groups[groupBy] = []dnssvcsv1.ResourceRecord{} 745 } 746 747 groups[groupBy] = append(groups[groupBy], r) 748 } 749 750 // create single endpoint with all the targets for each name/type 751 for _, records := range groups { 752 targets := make([]string, len(records)) 753 for i, record := range records { 754 data := record.Rdata 755 log.Debugf("record data: %v", data) 756 switch *record.Type { 757 case "A": 758 if !isNil(data["ip"]) { 759 targets[i] = data["ip"].(string) 760 } 761 case "CNAME": 762 if !isNil(data["cname"]) { 763 targets[i] = data["cname"].(string) 764 } 765 case "TXT": 766 if !isNil(data["text"]) { 767 targets[i] = data["text"].(string) 768 } 769 log.Debugf("text record data: %v", targets[i]) 770 } 771 } 772 773 ep := endpoint.NewEndpointWithTTL( 774 *records[0].Name, 775 *records[0].Type, 776 endpoint.TTL(*records[0].TTL), targets...) 777 778 log.Debugf( 779 "Found %s record for '%s' with target '%s'.", 780 ep.RecordType, 781 ep.DNSName, 782 ep.Targets, 783 ) 784 785 endpoints = append(endpoints, ep) 786 } 787 return endpoints 788 } 789 790 func (p *IBMCloudProvider) getPublicRecordID(records []dnsrecordsv1.DnsrecordDetails, record dnsrecordsv1.DnsrecordDetails) string { 791 for _, zoneRecord := range records { 792 if *zoneRecord.Name == *record.Name && *zoneRecord.Type == *record.Type && *zoneRecord.Content == *record.Content { 793 return *zoneRecord.ID 794 } 795 } 796 return "" 797 } 798 799 func (p *IBMCloudProvider) getPrivateRecordID(records []dnssvcsv1.ResourceRecord, record dnssvcsv1.ResourceRecord) string { 800 for _, zoneRecord := range records { 801 if *zoneRecord.Name == *record.Name && *zoneRecord.Type == *record.Type { 802 return *zoneRecord.ID 803 } 804 } 805 return "" 806 } 807 808 func (p *IBMCloudProvider) newIBMCloudChange(action string, endpoint *endpoint.Endpoint, target string) *ibmcloudChange { 809 ttl := defaultPublicRecordTTL 810 proxied := shouldBeProxied(endpoint, p.proxiedByDefault) 811 812 if endpoint.RecordTTL.IsConfigured() { 813 ttl = int(endpoint.RecordTTL) 814 } 815 816 if p.privateZone { 817 rData := make(map[string]interface{}) 818 switch endpoint.RecordType { 819 case "A": 820 rData[dnssvcsv1.CreateResourceRecordOptions_Type_A] = &dnssvcsv1.ResourceRecordInputRdataRdataARecord{ 821 Ip: core.StringPtr(target), 822 } 823 case "CNAME": 824 rData[dnssvcsv1.CreateResourceRecordOptions_Type_Cname] = &dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord{ 825 Cname: core.StringPtr(target), 826 } 827 case "TXT": 828 rData[dnssvcsv1.CreateResourceRecordOptions_Type_Txt] = &dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord{ 829 Text: core.StringPtr(target), 830 } 831 } 832 return &ibmcloudChange{ 833 Action: action, 834 PrivateResourceRecord: dnssvcsv1.ResourceRecord{ 835 Name: core.StringPtr(endpoint.DNSName), 836 TTL: core.Int64Ptr(int64(ttl)), 837 Type: core.StringPtr(endpoint.RecordType), 838 Rdata: rData, 839 }, 840 } 841 } 842 843 return &ibmcloudChange{ 844 Action: action, 845 PublicResourceRecord: dnsrecordsv1.DnsrecordDetails{ 846 Name: core.StringPtr(endpoint.DNSName), 847 TTL: core.Int64Ptr(int64(ttl)), 848 Proxied: core.BoolPtr(proxied), 849 Type: core.StringPtr(endpoint.RecordType), 850 Content: core.StringPtr(target), 851 }, 852 } 853 } 854 855 func (p *IBMCloudProvider) createRecord(ctx context.Context, zoneID string, change *ibmcloudChange) { 856 if p.privateZone { 857 createResourceRecordOptions := &dnssvcsv1.CreateResourceRecordOptions{ 858 InstanceID: core.StringPtr(p.instanceID), 859 DnszoneID: core.StringPtr(zoneID), 860 Name: change.PrivateResourceRecord.Name, 861 Type: change.PrivateResourceRecord.Type, 862 TTL: change.PrivateResourceRecord.TTL, 863 } 864 switch *change.PrivateResourceRecord.Type { 865 case "A": 866 data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_A].(*dnssvcsv1.ResourceRecordInputRdataRdataARecord) 867 aData, _ := p.Client.NewResourceRecordInputRdataRdataARecord(*data.Ip) 868 createResourceRecordOptions.SetRdata(aData) 869 case "CNAME": 870 data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Cname].(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord) 871 cnameData, _ := p.Client.NewResourceRecordInputRdataRdataCnameRecord(*data.Cname) 872 createResourceRecordOptions.SetRdata(cnameData) 873 case "TXT": 874 data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Txt].(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord) 875 txtData, _ := p.Client.NewResourceRecordInputRdataRdataTxtRecord(*data.Text) 876 createResourceRecordOptions.SetRdata(txtData) 877 } 878 _, _, err := p.Client.CreateResourceRecordWithContext(ctx, createResourceRecordOptions) 879 if err != nil { 880 log.Errorf("failed to create %s type record named %s: %v", *change.PrivateResourceRecord.Type, *change.PrivateResourceRecord.Name, err) 881 } 882 } else { 883 createDNSRecordOptions := &dnsrecordsv1.CreateDnsRecordOptions{ 884 Name: change.PublicResourceRecord.Name, 885 Type: change.PublicResourceRecord.Type, 886 TTL: change.PublicResourceRecord.TTL, 887 Content: change.PublicResourceRecord.Content, 888 } 889 _, _, err := p.Client.CreateDNSRecordWithContext(ctx, createDNSRecordOptions) 890 if err != nil { 891 log.Errorf("failed to create %s type record named %s: %v", *change.PublicResourceRecord.Type, *change.PublicResourceRecord.Name, err) 892 } 893 } 894 } 895 896 func (p *IBMCloudProvider) updateRecord(ctx context.Context, zoneID, recordID string, change *ibmcloudChange) { 897 if p.privateZone { 898 updateResourceRecordOptions := &dnssvcsv1.UpdateResourceRecordOptions{ 899 InstanceID: core.StringPtr(p.instanceID), 900 DnszoneID: core.StringPtr(zoneID), 901 RecordID: core.StringPtr(recordID), 902 Name: change.PrivateResourceRecord.Name, 903 TTL: change.PrivateResourceRecord.TTL, 904 } 905 switch *change.PrivateResourceRecord.Type { 906 case "A": 907 data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_A].(*dnssvcsv1.ResourceRecordInputRdataRdataARecord) 908 aData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataARecord(*data.Ip) 909 updateResourceRecordOptions.SetRdata(aData) 910 case "CNAME": 911 data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Cname].(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord) 912 cnameData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataCnameRecord(*data.Cname) 913 updateResourceRecordOptions.SetRdata(cnameData) 914 case "TXT": 915 data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Txt].(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord) 916 txtData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataTxtRecord(*data.Text) 917 updateResourceRecordOptions.SetRdata(txtData) 918 } 919 _, _, err := p.Client.UpdateResourceRecordWithContext(ctx, updateResourceRecordOptions) 920 if err != nil { 921 log.Errorf("failed to update %s type record named %s: %v", *change.PublicResourceRecord.Type, *change.PublicResourceRecord.Name, err) 922 } 923 } else { 924 updateDNSRecordOptions := &dnsrecordsv1.UpdateDnsRecordOptions{ 925 DnsrecordIdentifier: &recordID, 926 Name: change.PublicResourceRecord.Name, 927 Type: change.PublicResourceRecord.Type, 928 TTL: change.PublicResourceRecord.TTL, 929 Content: change.PublicResourceRecord.Content, 930 Proxied: change.PublicResourceRecord.Proxied, 931 } 932 _, _, err := p.Client.UpdateDNSRecordWithContext(ctx, updateDNSRecordOptions) 933 if err != nil { 934 log.Errorf("failed to update %s type record named %s: %v", *change.PublicResourceRecord.Type, *change.PublicResourceRecord.Name, err) 935 } 936 } 937 } 938 939 func (p *IBMCloudProvider) deleteRecord(ctx context.Context, zoneID, recordID string) { 940 if p.privateZone { 941 deleteResourceRecordOptions := &dnssvcsv1.DeleteResourceRecordOptions{ 942 InstanceID: core.StringPtr(p.instanceID), 943 DnszoneID: core.StringPtr(zoneID), 944 RecordID: core.StringPtr(recordID), 945 } 946 _, err := p.Client.DeleteResourceRecordWithContext(ctx, deleteResourceRecordOptions) 947 if err != nil { 948 log.Errorf("failed to delete record %s: %v", recordID, err) 949 } 950 } else { 951 deleteDNSRecordOptions := &dnsrecordsv1.DeleteDnsRecordOptions{ 952 DnsrecordIdentifier: &recordID, 953 } 954 _, _, err := p.Client.DeleteDNSRecordWithContext(ctx, deleteDNSRecordOptions) 955 if err != nil { 956 log.Errorf("failed to delete record %s: %v", recordID, err) 957 } 958 } 959 } 960 961 func shouldBeProxied(endpoint *endpoint.Endpoint, proxiedByDefault bool) bool { 962 proxied := proxiedByDefault 963 964 for _, v := range endpoint.ProviderSpecific { 965 if v.Name == proxyFilter { 966 b, err := strconv.ParseBool(v.Value) 967 if err != nil { 968 log.Errorf("Failed to parse annotation [%s]: %v", proxyFilter, err) 969 } else { 970 proxied = b 971 } 972 break 973 } 974 } 975 976 if proxyTypeNotSupported[endpoint.RecordType] || strings.Contains(endpoint.DNSName, "*") { 977 proxied = false 978 } 979 return proxied 980 } 981 982 func checkVPCAnnotation(endpoint *endpoint.Endpoint) string { 983 var vpc string 984 for _, v := range endpoint.ProviderSpecific { 985 if v.Name == vpcFilter { 986 vpcCrn, err := crn.Parse(v.Value) 987 if vpcCrn.ResourceType != "vpc" || err != nil { 988 log.Errorf("Failed to parse vpc [%s]: %v", v.Value, err) 989 } else { 990 vpc = v.Value 991 } 992 break 993 } 994 } 995 return vpc 996 } 997 998 func isNil(i interface{}) bool { 999 if i == nil { 1000 return true 1001 } 1002 switch reflect.TypeOf(i).Kind() { 1003 case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: 1004 return reflect.ValueOf(i).IsNil() 1005 } 1006 return false 1007 }