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  }