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

     1  /*
     2  Copyright 2017 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 infoblox
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net"
    23  	"net/http"
    24  	"os"
    25  	"sort"
    26  	"strconv"
    27  	"strings"
    28  
    29  	ibclient "github.com/infobloxopen/infoblox-go-client/v2"
    30  	"github.com/sirupsen/logrus"
    31  
    32  	"sigs.k8s.io/external-dns/endpoint"
    33  	"sigs.k8s.io/external-dns/pkg/rfc2317"
    34  	"sigs.k8s.io/external-dns/plan"
    35  	"sigs.k8s.io/external-dns/provider"
    36  )
    37  
    38  const (
    39  	// provider specific key to track if PTR record was already created or not for A records
    40  	providerSpecificInfobloxPtrRecord = "infoblox-ptr-record-exists"
    41  )
    42  
    43  func isNotFoundError(err error) bool {
    44  	_, ok := err.(*ibclient.NotFoundError)
    45  	return ok
    46  }
    47  
    48  // StartupConfig clarifies the method signature
    49  type StartupConfig struct {
    50  	DomainFilter  endpoint.DomainFilter
    51  	ZoneIDFilter  provider.ZoneIDFilter
    52  	Host          string
    53  	Port          int
    54  	Username      string
    55  	Password      string
    56  	Version       string
    57  	SSLVerify     bool
    58  	DryRun        bool
    59  	View          string
    60  	MaxResults    int
    61  	FQDNRegEx     string
    62  	NameRegEx     string
    63  	CreatePTR     bool
    64  	CacheDuration int
    65  }
    66  
    67  // ProviderConfig implements the DNS provider for Infoblox.
    68  type ProviderConfig struct {
    69  	provider.BaseProvider
    70  	client        ibclient.IBConnector
    71  	domainFilter  endpoint.DomainFilter
    72  	zoneIDFilter  provider.ZoneIDFilter
    73  	view          string
    74  	dryRun        bool
    75  	fqdnRegEx     string
    76  	createPTR     bool
    77  	cacheDuration int
    78  }
    79  
    80  type infobloxRecordSet struct {
    81  	obj ibclient.IBObject
    82  	res interface{}
    83  }
    84  
    85  // ExtendedRequestBuilder implements a HttpRequestBuilder which sets
    86  // additional query parameter on all get requests
    87  type ExtendedRequestBuilder struct {
    88  	fqdnRegEx  string
    89  	nameRegEx  string
    90  	maxResults int
    91  	ibclient.WapiRequestBuilder
    92  }
    93  
    94  // NewExtendedRequestBuilder returns a ExtendedRequestBuilder which adds
    95  // _max_results query parameter to all GET requests
    96  func NewExtendedRequestBuilder(maxResults int, fqdnRegEx string, nameRegEx string) *ExtendedRequestBuilder {
    97  	return &ExtendedRequestBuilder{
    98  		fqdnRegEx:  fqdnRegEx,
    99  		nameRegEx:  nameRegEx,
   100  		maxResults: maxResults,
   101  	}
   102  }
   103  
   104  // BuildRequest prepares the api request. it uses BuildRequest of
   105  // WapiRequestBuilder and then add the _max_requests parameter
   106  func (mrb *ExtendedRequestBuilder) BuildRequest(t ibclient.RequestType, obj ibclient.IBObject, ref string, queryParams *ibclient.QueryParams) (req *http.Request, err error) {
   107  	req, err = mrb.WapiRequestBuilder.BuildRequest(t, obj, ref, queryParams)
   108  	if req.Method == "GET" {
   109  		query := req.URL.Query()
   110  		if mrb.maxResults > 0 {
   111  			query.Set("_max_results", strconv.Itoa(mrb.maxResults))
   112  		}
   113  		_, zoneAuthQuery := obj.(*ibclient.ZoneAuth)
   114  		if zoneAuthQuery && t == ibclient.GET && mrb.fqdnRegEx != "" {
   115  			query.Set("fqdn~", mrb.fqdnRegEx)
   116  		}
   117  
   118  		// if we are not doing a ZoneAuth query, support the name filter
   119  		if !zoneAuthQuery && mrb.nameRegEx != "" {
   120  			query.Set("name~", mrb.nameRegEx)
   121  		}
   122  
   123  		req.URL.RawQuery = query.Encode()
   124  	}
   125  	return
   126  }
   127  
   128  // NewInfobloxProvider creates a new Infoblox provider.
   129  func NewInfobloxProvider(ibStartupCfg StartupConfig) (*ProviderConfig, error) {
   130  	hostCfg := ibclient.HostConfig{
   131  		Host:    ibStartupCfg.Host,
   132  		Port:    strconv.Itoa(ibStartupCfg.Port),
   133  		Version: ibStartupCfg.Version,
   134  	}
   135  
   136  	authCfg := ibclient.AuthConfig{
   137  		Username: ibStartupCfg.Username,
   138  		Password: ibStartupCfg.Password,
   139  	}
   140  
   141  	httpPoolConnections := lookupEnvAtoi("EXTERNAL_DNS_INFOBLOX_HTTP_POOL_CONNECTIONS", 10)
   142  	httpRequestTimeout := lookupEnvAtoi("EXTERNAL_DNS_INFOBLOX_HTTP_REQUEST_TIMEOUT", 60)
   143  
   144  	transportConfig := ibclient.NewTransportConfig(
   145  		strconv.FormatBool(ibStartupCfg.SSLVerify),
   146  		httpRequestTimeout,
   147  		httpPoolConnections,
   148  	)
   149  
   150  	var (
   151  		requestBuilder ibclient.HttpRequestBuilder
   152  		err            error
   153  	)
   154  	if ibStartupCfg.MaxResults != 0 || ibStartupCfg.FQDNRegEx != "" || ibStartupCfg.NameRegEx != "" {
   155  		// use our own HttpRequestBuilder which sets _max_results parameter on GET requests
   156  		requestBuilder = NewExtendedRequestBuilder(ibStartupCfg.MaxResults, ibStartupCfg.FQDNRegEx, ibStartupCfg.NameRegEx)
   157  	} else {
   158  		// use the default HttpRequestBuilder of the infoblox client
   159  		requestBuilder, err = ibclient.NewWapiRequestBuilder(hostCfg, authCfg)
   160  		if err != nil {
   161  			return nil, err
   162  		}
   163  	}
   164  
   165  	requestor := &ibclient.WapiHttpRequestor{}
   166  
   167  	client, err := ibclient.NewConnector(hostCfg, authCfg, transportConfig, requestBuilder, requestor)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	providerCfg := &ProviderConfig{
   173  		client:        client,
   174  		domainFilter:  ibStartupCfg.DomainFilter,
   175  		zoneIDFilter:  ibStartupCfg.ZoneIDFilter,
   176  		dryRun:        ibStartupCfg.DryRun,
   177  		view:          ibStartupCfg.View,
   178  		fqdnRegEx:     ibStartupCfg.FQDNRegEx,
   179  		createPTR:     ibStartupCfg.CreatePTR,
   180  		cacheDuration: ibStartupCfg.CacheDuration,
   181  	}
   182  
   183  	return providerCfg, nil
   184  }
   185  
   186  func recordQueryParams(zone string, view string) *ibclient.QueryParams {
   187  	searchFields := map[string]string{}
   188  	if zone != "" {
   189  		searchFields["zone"] = zone
   190  	}
   191  
   192  	if view != "" {
   193  		searchFields["view"] = view
   194  	}
   195  	return ibclient.NewQueryParams(false, searchFields)
   196  }
   197  
   198  // Records gets the current records.
   199  func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) {
   200  	zones, err := p.zones()
   201  	if err != nil {
   202  		return nil, fmt.Errorf("could not fetch zones: %w", err)
   203  	}
   204  
   205  	for _, zone := range zones {
   206  		logrus.Debugf("fetch records from zone '%s'", zone.Fqdn)
   207  		searchParams := recordQueryParams(zone.Fqdn, p.view)
   208  		var resA []ibclient.RecordA
   209  		objA := ibclient.NewEmptyRecordA()
   210  		err = p.client.GetObject(objA, "", searchParams, &resA)
   211  		if err != nil && !isNotFoundError(err) {
   212  			return nil, fmt.Errorf("could not fetch A records from zone '%s': %w", zone.Fqdn, err)
   213  		}
   214  		for _, res := range resA {
   215  			// Check if endpoint already exists and add to existing endpoint if it does
   216  			foundExisting := false
   217  			for _, ep := range endpoints {
   218  				if ep.DNSName == *res.Name && ep.RecordType == endpoint.RecordTypeA {
   219  					foundExisting = true
   220  					duplicateTarget := false
   221  
   222  					for _, t := range ep.Targets {
   223  						if t == *res.Ipv4Addr {
   224  							duplicateTarget = true
   225  							break
   226  						}
   227  					}
   228  
   229  					if duplicateTarget {
   230  						logrus.Debugf("A duplicate target '%s' found for existing A record '%s'", *res.Ipv4Addr, ep.DNSName)
   231  					} else {
   232  						logrus.Debugf("Adding target '%s' to existing A record '%s'", *res.Ipv4Addr, *res.Name)
   233  						ep.Targets = append(ep.Targets, *res.Ipv4Addr)
   234  					}
   235  					break
   236  				}
   237  			}
   238  			if !foundExisting {
   239  				newEndpoint := endpoint.NewEndpoint(*res.Name, endpoint.RecordTypeA, *res.Ipv4Addr)
   240  				if p.createPTR {
   241  					newEndpoint.WithProviderSpecific(providerSpecificInfobloxPtrRecord, "true")
   242  				}
   243  				endpoints = append(endpoints, newEndpoint)
   244  			}
   245  		}
   246  		// sort targets so that they are always in same order, as infoblox might return them in different order
   247  		for _, ep := range endpoints {
   248  			sort.Sort(ep.Targets)
   249  		}
   250  
   251  		// Include Host records since they should be treated synonymously with A records
   252  		var resH []ibclient.HostRecord
   253  		objH := ibclient.NewEmptyHostRecord()
   254  		err = p.client.GetObject(objH, "", searchParams, &resH)
   255  		if err != nil && !isNotFoundError(err) {
   256  			return nil, fmt.Errorf("could not fetch host records from zone '%s': %w", zone.Fqdn, err)
   257  		}
   258  		for _, res := range resH {
   259  			for _, ip := range res.Ipv4Addrs {
   260  				logrus.Debugf("Record='%s' A(H):'%s'", *res.Name, *ip.Ipv4Addr)
   261  
   262  				// host record is an abstraction in infoblox that combines A and PTR records
   263  				// for any host record we already should have a PTR record in infoblox, so mark it as created
   264  				newEndpoint := endpoint.NewEndpoint(*res.Name, endpoint.RecordTypeA, *ip.Ipv4Addr)
   265  				if p.createPTR {
   266  					newEndpoint.WithProviderSpecific(providerSpecificInfobloxPtrRecord, "true")
   267  				}
   268  				endpoints = append(endpoints, newEndpoint)
   269  			}
   270  		}
   271  
   272  		var resC []ibclient.RecordCNAME
   273  		objC := ibclient.NewEmptyRecordCNAME()
   274  		err = p.client.GetObject(objC, "", searchParams, &resC)
   275  		if err != nil && !isNotFoundError(err) {
   276  			return nil, fmt.Errorf("could not fetch CNAME records from zone '%s': %w", zone.Fqdn, err)
   277  		}
   278  		for _, res := range resC {
   279  			logrus.Debugf("Record='%s' CNAME:'%s'", *res.Name, *res.Canonical)
   280  			endpoints = append(endpoints, endpoint.NewEndpoint(*res.Name, endpoint.RecordTypeCNAME, *res.Canonical))
   281  		}
   282  
   283  		if p.createPTR {
   284  			// infoblox doesn't accept reverse zone's fqdn, and instead expects .in-addr.arpa zone
   285  			// so convert our zone fqdn (if it is a correct cidr block) into in-addr.arpa address and pass that into infoblox
   286  			// example: 10.196.38.0/24 becomes 38.196.10.in-addr.arpa
   287  			arpaZone, err := rfc2317.CidrToInAddr(zone.Fqdn)
   288  			if err == nil {
   289  				var resP []ibclient.RecordPTR
   290  				objP := ibclient.NewEmptyRecordPTR()
   291  				err = p.client.GetObject(objP, "", recordQueryParams(arpaZone, p.view), &resP)
   292  				if err != nil && !isNotFoundError(err) {
   293  					return nil, fmt.Errorf("could not fetch PTR records from zone '%s': %w", zone.Fqdn, err)
   294  				}
   295  				for _, res := range resP {
   296  					endpoints = append(endpoints, endpoint.NewEndpoint(*res.PtrdName, endpoint.RecordTypePTR, *res.Ipv4Addr))
   297  				}
   298  			}
   299  		}
   300  
   301  		var resT []ibclient.RecordTXT
   302  		objT := ibclient.NewEmptyRecordTXT()
   303  		err = p.client.GetObject(objT, "", searchParams, &resT)
   304  		if err != nil && !isNotFoundError(err) {
   305  			return nil, fmt.Errorf("could not fetch TXT records from zone '%s': %w", zone.Fqdn, err)
   306  		}
   307  		for _, res := range resT {
   308  			// The Infoblox API strips enclosing double quotes from TXT records lacking whitespace.
   309  			// Unhandled, the missing double quotes would break the extractOwnerID method of the registry package.
   310  			if _, err := strconv.Unquote(*res.Text); err != nil {
   311  				quoted := strconv.Quote(*res.Text)
   312  				res.Text = &quoted
   313  			}
   314  
   315  			foundExisting := false
   316  
   317  			for _, ep := range endpoints {
   318  				if ep.DNSName == *res.Name && ep.RecordType == endpoint.RecordTypeTXT {
   319  					foundExisting = true
   320  					duplicateTarget := false
   321  
   322  					for _, t := range ep.Targets {
   323  						if t == *res.Text {
   324  							duplicateTarget = true
   325  							break
   326  						}
   327  					}
   328  
   329  					if duplicateTarget {
   330  						logrus.Debugf("A duplicate target '%s' found for existing TXT record '%s'", *res.Text, ep.DNSName)
   331  					} else {
   332  						logrus.Debugf("Adding target '%s' to existing TXT record '%s'", *res.Text, *res.Name)
   333  						ep.Targets = append(ep.Targets, *res.Text)
   334  					}
   335  					break
   336  				}
   337  			}
   338  			if !foundExisting {
   339  				logrus.Debugf("Record='%s' TXT:'%s'", *res.Name, *res.Text)
   340  				newEndpoint := endpoint.NewEndpoint(*res.Name, endpoint.RecordTypeTXT, *res.Text)
   341  				endpoints = append(endpoints, newEndpoint)
   342  			}
   343  		}
   344  	}
   345  
   346  	// update A records that have PTR record created for them already
   347  	if p.createPTR {
   348  		// save all ptr records into map for a quick look up
   349  		ptrRecordsMap := make(map[string]bool)
   350  		for _, ptrRecord := range endpoints {
   351  			if ptrRecord.RecordType != endpoint.RecordTypePTR {
   352  				continue
   353  			}
   354  			ptrRecordsMap[ptrRecord.DNSName] = true
   355  		}
   356  
   357  		for i := range endpoints {
   358  			if endpoints[i].RecordType != endpoint.RecordTypeA {
   359  				continue
   360  			}
   361  			// if PTR record already exists for A record, then mark it as such
   362  			if ptrRecordsMap[endpoints[i].DNSName] {
   363  				found := false
   364  				for j := range endpoints[i].ProviderSpecific {
   365  					if endpoints[i].ProviderSpecific[j].Name == providerSpecificInfobloxPtrRecord {
   366  						endpoints[i].ProviderSpecific[j].Value = "true"
   367  						found = true
   368  					}
   369  				}
   370  				if !found {
   371  					endpoints[i].WithProviderSpecific(providerSpecificInfobloxPtrRecord, "true")
   372  				}
   373  			}
   374  		}
   375  	}
   376  	logrus.Debugf("fetched %d records from infoblox", len(endpoints))
   377  	return endpoints, nil
   378  }
   379  
   380  func (p *ProviderConfig) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) {
   381  	// Update user specified TTL (0 == disabled)
   382  	for i := range endpoints {
   383  		endpoints[i].RecordTTL = endpoint.TTL(p.cacheDuration)
   384  	}
   385  
   386  	if !p.createPTR {
   387  		return endpoints, nil
   388  	}
   389  
   390  	// for all A records, we want to create PTR records
   391  	// so add provider specific property to track if the record was created or not
   392  	for i := range endpoints {
   393  		if endpoints[i].RecordType == endpoint.RecordTypeA {
   394  			found := false
   395  			for j := range endpoints[i].ProviderSpecific {
   396  				if endpoints[i].ProviderSpecific[j].Name == providerSpecificInfobloxPtrRecord {
   397  					endpoints[i].ProviderSpecific[j].Value = "true"
   398  					found = true
   399  				}
   400  			}
   401  			if !found {
   402  				endpoints[i].WithProviderSpecific(providerSpecificInfobloxPtrRecord, "true")
   403  			}
   404  		}
   405  	}
   406  
   407  	return endpoints, nil
   408  }
   409  
   410  // ApplyChanges applies the given changes.
   411  func (p *ProviderConfig) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
   412  	zones, err := p.zones()
   413  	if err != nil {
   414  		return err
   415  	}
   416  
   417  	created, deleted := p.mapChanges(zones, changes)
   418  	p.deleteRecords(deleted)
   419  	p.createRecords(created)
   420  	return nil
   421  }
   422  
   423  func (p *ProviderConfig) zones() ([]ibclient.ZoneAuth, error) {
   424  	var res, result []ibclient.ZoneAuth
   425  	obj := ibclient.NewZoneAuth(ibclient.ZoneAuth{})
   426  	queryParams := recordQueryParams("", p.view)
   427  	err := p.client.GetObject(obj, "", queryParams, &res)
   428  	if err != nil && !isNotFoundError(err) {
   429  		return nil, err
   430  	}
   431  
   432  	for _, zone := range res {
   433  		if !p.domainFilter.Match(zone.Fqdn) {
   434  			continue
   435  		}
   436  
   437  		if !p.zoneIDFilter.Match(zone.Ref) {
   438  			continue
   439  		}
   440  
   441  		result = append(result, zone)
   442  	}
   443  
   444  	return result, nil
   445  }
   446  
   447  type infobloxChangeMap map[string][]*endpoint.Endpoint
   448  
   449  func (p *ProviderConfig) mapChanges(zones []ibclient.ZoneAuth, changes *plan.Changes) (infobloxChangeMap, infobloxChangeMap) {
   450  	created := infobloxChangeMap{}
   451  	deleted := infobloxChangeMap{}
   452  
   453  	mapChange := func(changeMap infobloxChangeMap, change *endpoint.Endpoint) {
   454  		zone := p.findZone(zones, change.DNSName)
   455  		if zone == nil {
   456  			logrus.Debugf("Ignoring changes to '%s' because a suitable Infoblox DNS zone was not found.", change.DNSName)
   457  			return
   458  		}
   459  		// Ensure the record type is suitable
   460  		changeMap[zone.Fqdn] = append(changeMap[zone.Fqdn], change)
   461  
   462  		if p.createPTR && change.RecordType == endpoint.RecordTypeA {
   463  			reverseZone := p.findReverseZone(zones, change.Targets[0])
   464  			if reverseZone == nil {
   465  				logrus.Debugf("Ignoring changes to '%s' because a suitable Infoblox DNS reverse zone was not found.", change.Targets[0])
   466  				return
   467  			}
   468  			changecopy := *change
   469  			changecopy.RecordType = endpoint.RecordTypePTR
   470  			changeMap[reverseZone.Fqdn] = append(changeMap[reverseZone.Fqdn], &changecopy)
   471  		}
   472  	}
   473  
   474  	for _, change := range changes.Delete {
   475  		mapChange(deleted, change)
   476  	}
   477  	for _, change := range changes.UpdateOld {
   478  		mapChange(deleted, change)
   479  	}
   480  	for _, change := range changes.Create {
   481  		mapChange(created, change)
   482  	}
   483  	for _, change := range changes.UpdateNew {
   484  		mapChange(created, change)
   485  	}
   486  
   487  	return created, deleted
   488  }
   489  
   490  func (p *ProviderConfig) findZone(zones []ibclient.ZoneAuth, name string) *ibclient.ZoneAuth {
   491  	var result *ibclient.ZoneAuth
   492  
   493  	// Go through every zone looking for the longest name (i.e. most specific) as a matching suffix
   494  	for idx := range zones {
   495  		zone := &zones[idx]
   496  		if strings.HasSuffix(name, "."+zone.Fqdn) {
   497  			if result == nil || len(zone.Fqdn) > len(result.Fqdn) {
   498  				result = zone
   499  			}
   500  		} else if strings.EqualFold(name, zone.Fqdn) {
   501  			if result == nil || len(zone.Fqdn) > len(result.Fqdn) {
   502  				result = zone
   503  			}
   504  		}
   505  	}
   506  	return result
   507  }
   508  
   509  func (p *ProviderConfig) findReverseZone(zones []ibclient.ZoneAuth, name string) *ibclient.ZoneAuth {
   510  	ip := net.ParseIP(name)
   511  	networks := map[int]*ibclient.ZoneAuth{}
   512  	maxMask := 0
   513  
   514  	for i, zone := range zones {
   515  		_, rZoneNet, err := net.ParseCIDR(zone.Fqdn)
   516  		if err != nil {
   517  			logrus.WithError(err).Debugf("fqdn %s is no cidr", zone.Fqdn)
   518  		} else {
   519  			if rZoneNet.Contains(ip) {
   520  				_, mask := rZoneNet.Mask.Size()
   521  				networks[mask] = &zones[i]
   522  				if mask > maxMask {
   523  					maxMask = mask
   524  				}
   525  			}
   526  		}
   527  	}
   528  	return networks[maxMask]
   529  }
   530  
   531  func (p *ProviderConfig) recordSet(ep *endpoint.Endpoint, getObject bool, targetIndex int) (recordSet infobloxRecordSet, err error) {
   532  	switch ep.RecordType {
   533  	case endpoint.RecordTypeA:
   534  		var res []ibclient.RecordA
   535  		obj := ibclient.NewEmptyRecordA()
   536  		obj.Name = &ep.DNSName
   537  		obj.Ipv4Addr = &ep.Targets[targetIndex]
   538  		obj.View = p.view
   539  		if getObject {
   540  			queryParams := ibclient.NewQueryParams(false, map[string]string{"name": *obj.Name})
   541  			err = p.client.GetObject(obj, "", queryParams, &res)
   542  			if err != nil && !isNotFoundError(err) {
   543  				return
   544  			}
   545  		}
   546  		recordSet = infobloxRecordSet{
   547  			obj: obj,
   548  			res: &res,
   549  		}
   550  	case endpoint.RecordTypePTR:
   551  		var res []ibclient.RecordPTR
   552  		obj := ibclient.NewEmptyRecordPTR()
   553  		obj.PtrdName = &ep.DNSName
   554  		obj.Ipv4Addr = &ep.Targets[targetIndex]
   555  		obj.View = p.view
   556  		if getObject {
   557  			queryParams := ibclient.NewQueryParams(false, map[string]string{"name": *obj.PtrdName})
   558  			err = p.client.GetObject(obj, "", queryParams, &res)
   559  			if err != nil && !isNotFoundError(err) {
   560  				return
   561  			}
   562  		}
   563  		recordSet = infobloxRecordSet{
   564  			obj: obj,
   565  			res: &res,
   566  		}
   567  	case endpoint.RecordTypeCNAME:
   568  		var res []ibclient.RecordCNAME
   569  		obj := ibclient.NewEmptyRecordCNAME()
   570  		obj.Name = &ep.DNSName
   571  		obj.Canonical = &ep.Targets[0]
   572  		obj.View = &p.view
   573  		if getObject {
   574  			queryParams := ibclient.NewQueryParams(false, map[string]string{"name": *obj.Name})
   575  			err = p.client.GetObject(obj, "", queryParams, &res)
   576  			if err != nil && !isNotFoundError(err) {
   577  				return
   578  			}
   579  		}
   580  		recordSet = infobloxRecordSet{
   581  			obj: obj,
   582  			res: &res,
   583  		}
   584  	case endpoint.RecordTypeTXT:
   585  		var res []ibclient.RecordTXT
   586  		// The Infoblox API strips enclosing double quotes from TXT records lacking whitespace.
   587  		// Here we reconcile that fact by making this state match that reality.
   588  		if target, err2 := strconv.Unquote(ep.Targets[0]); err2 == nil && !strings.Contains(ep.Targets[0], " ") {
   589  			ep.Targets = endpoint.Targets{target}
   590  		}
   591  		obj := ibclient.NewEmptyRecordTXT()
   592  		obj.Name = &ep.DNSName
   593  		obj.Text = &ep.Targets[0]
   594  		obj.View = &p.view
   595  		if getObject {
   596  			queryParams := ibclient.NewQueryParams(false, map[string]string{"name": *obj.Name})
   597  			err = p.client.GetObject(obj, "", queryParams, &res)
   598  			if err != nil && !isNotFoundError(err) {
   599  				return
   600  			}
   601  		}
   602  		recordSet = infobloxRecordSet{
   603  			obj: obj,
   604  			res: &res,
   605  		}
   606  	}
   607  	return
   608  }
   609  
   610  func (p *ProviderConfig) createRecords(created infobloxChangeMap) {
   611  	for zone, endpoints := range created {
   612  		for _, ep := range endpoints {
   613  			for targetIndex := range ep.Targets {
   614  				if p.dryRun {
   615  					logrus.Infof(
   616  
   617  						"Would create %s record named '%s' to '%s' for Infoblox DNS zone '%s'.",
   618  						ep.RecordType,
   619  						ep.DNSName,
   620  						ep.Targets[targetIndex],
   621  						zone,
   622  					)
   623  					continue
   624  				}
   625  
   626  				logrus.Infof(
   627  					"Creating %s record named '%s' to '%s' for Infoblox DNS zone '%s'.",
   628  					ep.RecordType,
   629  					ep.DNSName,
   630  					ep.Targets[targetIndex],
   631  					zone,
   632  				)
   633  
   634  				recordSet, err := p.recordSet(ep, false, targetIndex)
   635  				if err != nil && !isNotFoundError(err) {
   636  					logrus.Errorf(
   637  						"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
   638  						ep.RecordType,
   639  						ep.DNSName,
   640  						ep.Targets[targetIndex],
   641  						zone,
   642  						err,
   643  					)
   644  					continue
   645  				}
   646  				_, err = p.client.CreateObject(recordSet.obj)
   647  				if err != nil {
   648  					logrus.Errorf(
   649  						"Failed to create %s record named '%s' to '%s' for DNS zone '%s': %v",
   650  						ep.RecordType,
   651  						ep.DNSName,
   652  						ep.Targets[targetIndex],
   653  						zone,
   654  						err,
   655  					)
   656  				}
   657  			}
   658  		}
   659  	}
   660  }
   661  
   662  func (p *ProviderConfig) deleteRecords(deleted infobloxChangeMap) {
   663  	// Delete records first
   664  	for zone, endpoints := range deleted {
   665  		for _, ep := range endpoints {
   666  			for targetIndex := range ep.Targets {
   667  				recordSet, err := p.recordSet(ep, true, targetIndex)
   668  				if err != nil && !isNotFoundError(err) {
   669  					logrus.Errorf(
   670  						"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
   671  						ep.RecordType,
   672  						ep.DNSName,
   673  						ep.Targets[targetIndex],
   674  						zone,
   675  						err,
   676  					)
   677  					continue
   678  				}
   679  				switch ep.RecordType {
   680  				case endpoint.RecordTypeA:
   681  					for _, record := range *recordSet.res.(*[]ibclient.RecordA) {
   682  						if p.dryRun {
   683  							logrus.Infof("Would delete %s record named '%p' to '%p' for Infoblox DNS zone '%s'.", "A", record.Name, record.Ipv4Addr, record.Zone)
   684  						} else {
   685  							logrus.Infof("Deleting %s record named '%p' to '%p' for Infoblox DNS zone '%s'.", "A", record.Name, record.Ipv4Addr, record.Zone)
   686  							_, err = p.client.DeleteObject(record.Ref)
   687  						}
   688  					}
   689  				case endpoint.RecordTypePTR:
   690  					for _, record := range *recordSet.res.(*[]ibclient.RecordPTR) {
   691  						if p.dryRun {
   692  							logrus.Infof("Would delete %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "PTR", *record.PtrdName, *record.Ipv4Addr, record.Zone)
   693  						} else {
   694  							logrus.Infof("Deleting %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "PTR", *record.PtrdName, *record.Ipv4Addr, record.Zone)
   695  							_, err = p.client.DeleteObject(record.Ref)
   696  						}
   697  					}
   698  				case endpoint.RecordTypeCNAME:
   699  					for _, record := range *recordSet.res.(*[]ibclient.RecordCNAME) {
   700  						if p.dryRun {
   701  							logrus.Infof("Would delete %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "CNAME", *record.Name, *record.Canonical, record.Zone)
   702  						} else {
   703  							logrus.Infof("Deleting %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "CNAME", *record.Name, *record.Canonical, record.Zone)
   704  							_, err = p.client.DeleteObject(record.Ref)
   705  						}
   706  					}
   707  				case endpoint.RecordTypeTXT:
   708  					for _, record := range *recordSet.res.(*[]ibclient.RecordTXT) {
   709  						if p.dryRun {
   710  							logrus.Infof("Would delete %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "TXT", *record.Name, *record.Text, record.Zone)
   711  						} else {
   712  							logrus.Infof("Deleting %s record named '%s' to '%s' for Infoblox DNS zone '%s'.", "TXT", *record.Name, *record.Text, record.Zone)
   713  							_, err = p.client.DeleteObject(record.Ref)
   714  						}
   715  					}
   716  				}
   717  				if err != nil && !isNotFoundError(err) {
   718  					logrus.Errorf(
   719  						"Failed to delete %s record named '%s' to '%s' for Infoblox DNS zone '%s': %v",
   720  						ep.RecordType,
   721  						ep.DNSName,
   722  						ep.Targets[targetIndex],
   723  						zone,
   724  						err,
   725  					)
   726  				}
   727  			}
   728  		}
   729  	}
   730  }
   731  
   732  func lookupEnvAtoi(key string, fallback int) (i int) {
   733  	val, ok := os.LookupEnv(key)
   734  	if !ok {
   735  		i = fallback
   736  		return
   737  	}
   738  	i, err := strconv.Atoi(val)
   739  	if err != nil {
   740  		i = fallback
   741  		return
   742  	}
   743  	return
   744  }