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

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  // TODO: add logging
    17  // TODO: add timeouts
    18  package api
    19  
    20  import (
    21  	"bytes"
    22  	"crypto/tls"
    23  	"encoding/json"
    24  	"io"
    25  	"net/http"
    26  	"os"
    27  	"strings"
    28  
    29  	"github.com/pkg/errors"
    30  	log "github.com/sirupsen/logrus"
    31  )
    32  
    33  // TODO: Ensure DNS Deploy Type Defaults to no-deploy instead of ""
    34  type BluecatConfig struct {
    35  	GatewayHost      string `json:"gatewayHost"`
    36  	GatewayUsername  string `json:"gatewayUsername,omitempty"`
    37  	GatewayPassword  string `json:"gatewayPassword,omitempty"`
    38  	DNSConfiguration string `json:"dnsConfiguration"`
    39  	DNSServerName    string `json:"dnsServerName"`
    40  	DNSDeployType    string `json:"dnsDeployType"`
    41  	View             string `json:"dnsView"`
    42  	RootZone         string `json:"rootZone"`
    43  	SkipTLSVerify    bool   `json:"skipTLSVerify"`
    44  }
    45  
    46  type GatewayClient interface {
    47  	GetBluecatZones(zoneName string) ([]BluecatZone, error)
    48  	GetHostRecords(zone string, records *[]BluecatHostRecord) error
    49  	GetCNAMERecords(zone string, records *[]BluecatCNAMERecord) error
    50  	GetHostRecord(name string, record *BluecatHostRecord) error
    51  	GetCNAMERecord(name string, record *BluecatCNAMERecord) error
    52  	CreateHostRecord(zone string, req *BluecatCreateHostRecordRequest) error
    53  	CreateCNAMERecord(zone string, req *BluecatCreateCNAMERecordRequest) error
    54  	DeleteHostRecord(name string, zone string) (err error)
    55  	DeleteCNAMERecord(name string, zone string) (err error)
    56  	GetTXTRecords(zone string, records *[]BluecatTXTRecord) error
    57  	GetTXTRecord(name string, record *BluecatTXTRecord) error
    58  	CreateTXTRecord(zone string, req *BluecatCreateTXTRecordRequest) error
    59  	DeleteTXTRecord(name string, zone string) error
    60  	ServerFullDeploy() error
    61  }
    62  
    63  // GatewayClientConfig defines the configuration for a Bluecat Gateway Client
    64  type GatewayClientConfig struct {
    65  	Cookie           http.Cookie
    66  	Token            string
    67  	Host             string
    68  	DNSConfiguration string
    69  	View             string
    70  	RootZone         string
    71  	DNSServerName    string
    72  	SkipTLSVerify    bool
    73  }
    74  
    75  // BluecatZone defines a zone to hold records
    76  type BluecatZone struct {
    77  	ID         int    `json:"id"`
    78  	Name       string `json:"name"`
    79  	Properties string `json:"properties"`
    80  	Type       string `json:"type"`
    81  }
    82  
    83  // BluecatHostRecord defines dns Host record
    84  type BluecatHostRecord struct {
    85  	ID         int    `json:"id"`
    86  	Name       string `json:"name"`
    87  	Properties string `json:"properties"`
    88  	Type       string `json:"type"`
    89  }
    90  
    91  // BluecatCNAMERecord defines dns CNAME record
    92  type BluecatCNAMERecord struct {
    93  	ID         int    `json:"id"`
    94  	Name       string `json:"name"`
    95  	Properties string `json:"properties"`
    96  	Type       string `json:"type"`
    97  }
    98  
    99  // BluecatTXTRecord defines dns TXT record
   100  type BluecatTXTRecord struct {
   101  	ID         int    `json:"id"`
   102  	Name       string `json:"name"`
   103  	Properties string `json:"properties"`
   104  }
   105  
   106  type BluecatCreateHostRecordRequest struct {
   107  	AbsoluteName string `json:"absolute_name"`
   108  	IP4Address   string `json:"ip4_address"`
   109  	TTL          int    `json:"ttl"`
   110  	Properties   string `json:"properties"`
   111  }
   112  
   113  type BluecatCreateCNAMERecordRequest struct {
   114  	AbsoluteName string `json:"absolute_name"`
   115  	LinkedRecord string `json:"linked_record"`
   116  	TTL          int    `json:"ttl"`
   117  	Properties   string `json:"properties"`
   118  }
   119  
   120  type BluecatCreateTXTRecordRequest struct {
   121  	AbsoluteName string `json:"absolute_name"`
   122  	Text         string `json:"txt"`
   123  }
   124  
   125  type BluecatServerFullDeployRequest struct {
   126  	ServerName string `json:"server_name"`
   127  }
   128  
   129  // NewGatewayClient creates and returns a new Bluecat gateway client
   130  func NewGatewayClientConfig(cookie http.Cookie, token, gatewayHost, dnsConfiguration, view, rootZone, dnsServerName string, skipTLSVerify bool) GatewayClientConfig {
   131  	// TODO: do not handle defaulting here
   132  	//
   133  	// Right now the Bluecat gateway doesn't seem to have a way to get the root zone from the API. If the user
   134  	// doesn't provide one via the config file we'll assume it's 'com'
   135  	if rootZone == "" {
   136  		rootZone = "com"
   137  	}
   138  	return GatewayClientConfig{
   139  		Cookie:           cookie,
   140  		Token:            token,
   141  		Host:             gatewayHost,
   142  		DNSConfiguration: dnsConfiguration,
   143  		DNSServerName:    dnsServerName,
   144  		View:             view,
   145  		RootZone:         rootZone,
   146  		SkipTLSVerify:    skipTLSVerify,
   147  	}
   148  }
   149  
   150  // GetBluecatGatewayToken retrieves a Bluecat Gateway API token.
   151  func GetBluecatGatewayToken(cfg BluecatConfig) (string, http.Cookie, error) {
   152  	var username string
   153  	if cfg.GatewayUsername != "" {
   154  		username = cfg.GatewayUsername
   155  	}
   156  	if v, ok := os.LookupEnv("BLUECAT_USERNAME"); ok {
   157  		username = v
   158  	}
   159  
   160  	var password string
   161  	if cfg.GatewayPassword != "" {
   162  		password = cfg.GatewayPassword
   163  	}
   164  	if v, ok := os.LookupEnv("BLUECAT_PASSWORD"); ok {
   165  		password = v
   166  	}
   167  
   168  	body, err := json.Marshal(map[string]string{
   169  		"username": username,
   170  		"password": password,
   171  	})
   172  	if err != nil {
   173  		return "", http.Cookie{}, errors.Wrap(err, "could not unmarshal credentials for bluecat gateway config")
   174  	}
   175  	url := cfg.GatewayHost + "/rest_login"
   176  
   177  	response, err := executeHTTPRequest(cfg.SkipTLSVerify, http.MethodPost, url, "", bytes.NewBuffer(body), http.Cookie{})
   178  	if err != nil {
   179  		return "", http.Cookie{}, errors.Wrap(err, "error obtaining API token from bluecat gateway")
   180  	}
   181  	defer response.Body.Close()
   182  
   183  	responseBody, err := io.ReadAll(response.Body)
   184  	if err != nil {
   185  		return "", http.Cookie{}, errors.Wrap(err, "failed to read login response from bluecat gateway")
   186  	}
   187  
   188  	if response.StatusCode != http.StatusOK {
   189  		return "", http.Cookie{}, errors.Errorf("got HTTP response code %v, detailed message: %v", response.StatusCode, string(responseBody))
   190  	}
   191  
   192  	jsonResponse := map[string]string{}
   193  	err = json.Unmarshal(responseBody, &jsonResponse)
   194  	if err != nil {
   195  		return "", http.Cookie{}, errors.Wrap(err, "error unmarshaling json response (auth) from bluecat gateway")
   196  	}
   197  
   198  	// Example response: {"access_token": "BAMAuthToken: abc123"}
   199  	// We only care about the actual token string - i.e. abc123
   200  	// The gateway also creates a cookie as part of the response. This seems to be the actual auth mechanism, at least
   201  	// for now.
   202  	return strings.Split(jsonResponse["access_token"], " ")[1], *response.Cookies()[0], nil
   203  }
   204  
   205  func (c GatewayClientConfig) GetBluecatZones(zoneName string) ([]BluecatZone, error) {
   206  	zonePath := expandZone(zoneName)
   207  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath
   208  
   209  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
   210  	if err != nil {
   211  		return nil, errors.Wrapf(err, "error requesting zones from gateway: %v, %v", url, zoneName)
   212  	}
   213  	defer response.Body.Close()
   214  
   215  	if response.StatusCode != http.StatusOK {
   216  		return nil, errors.Errorf("received http %v requesting zones from gateway in zone %v", response.StatusCode, zoneName)
   217  	}
   218  
   219  	zones := []BluecatZone{}
   220  	json.NewDecoder(response.Body).Decode(&zones)
   221  
   222  	// Bluecat Gateway only returns subzones one level deeper than the provided zone
   223  	// so this recursion is needed to traverse subzones until none are returned
   224  	for _, zone := range zones {
   225  		zoneProps := SplitProperties(zone.Properties)
   226  		subZones, err := c.GetBluecatZones(zoneProps["absoluteName"])
   227  		if err != nil {
   228  			return nil, errors.Wrapf(err, "error retrieving subzones from gateway: %v", zoneName)
   229  		}
   230  		zones = append(zones, subZones...)
   231  	}
   232  
   233  	return zones, nil
   234  }
   235  
   236  func (c GatewayClientConfig) GetHostRecords(zone string, records *[]BluecatHostRecord) error {
   237  	zonePath := expandZone(zone)
   238  	// Remove the trailing 'zones/'
   239  	zonePath = strings.TrimSuffix(zonePath, "zones/")
   240  
   241  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/"
   242  
   243  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
   244  	if err != nil {
   245  		return errors.Wrapf(err, "error requesting host records from gateway in zone %v", zone)
   246  	}
   247  	defer response.Body.Close()
   248  
   249  	if response.StatusCode != http.StatusOK {
   250  		return errors.Errorf("received http %v requesting host records from gateway in zone %v", response.StatusCode, zone)
   251  	}
   252  
   253  	json.NewDecoder(response.Body).Decode(records)
   254  	log.Debugf("Get Host Records Response: %v", records)
   255  
   256  	return nil
   257  }
   258  
   259  func (c GatewayClientConfig) GetCNAMERecords(zone string, records *[]BluecatCNAMERecord) error {
   260  	zonePath := expandZone(zone)
   261  	// Remove the trailing 'zones/'
   262  	zonePath = strings.TrimSuffix(zonePath, "zones/")
   263  
   264  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/"
   265  
   266  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
   267  	if err != nil {
   268  		return errors.Wrapf(err, "error retrieving cname records from gateway in zone %v", zone)
   269  	}
   270  	defer response.Body.Close()
   271  
   272  	if response.StatusCode != http.StatusOK {
   273  		return errors.Errorf("received http %v requesting cname records from gateway in zone %v", response.StatusCode, zone)
   274  	}
   275  
   276  	json.NewDecoder(response.Body).Decode(records)
   277  	log.Debugf("Get CName Records Response: %v", records)
   278  
   279  	return nil
   280  }
   281  
   282  func (c GatewayClientConfig) GetTXTRecords(zone string, records *[]BluecatTXTRecord) error {
   283  	zonePath := expandZone(zone)
   284  	// Remove the trailing 'zones/'
   285  	zonePath = strings.TrimSuffix(zonePath, "zones/")
   286  
   287  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/"
   288  
   289  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
   290  	if err != nil {
   291  		return errors.Wrapf(err, "error retrieving txt records from gateway in zone %v", zone)
   292  	}
   293  	defer response.Body.Close()
   294  
   295  	if response.StatusCode != http.StatusOK {
   296  		return errors.Errorf("received http %v requesting txt records from gateway in zone %v", response.StatusCode, zone)
   297  	}
   298  
   299  	log.Debugf("Get Txt Records response: %v", response)
   300  	json.NewDecoder(response.Body).Decode(records)
   301  	log.Debugf("Get TXT Records Body: %v", records)
   302  
   303  	return nil
   304  }
   305  
   306  func (c GatewayClientConfig) GetHostRecord(name string, record *BluecatHostRecord) error {
   307  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
   308  		"/views/" + c.View + "/" +
   309  		"host_records/" + name + "/"
   310  
   311  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
   312  	if err != nil {
   313  		return errors.Wrapf(err, "error retrieving host record %v from gateway", name)
   314  	}
   315  	defer response.Body.Close()
   316  
   317  	if response.StatusCode != http.StatusOK {
   318  		return errors.Errorf("received http %v while retrieving host record %v from gateway", response.StatusCode, name)
   319  	}
   320  
   321  	json.NewDecoder(response.Body).Decode(record)
   322  	log.Debugf("Get Host Record Response: %v", record)
   323  	return nil
   324  }
   325  
   326  func (c GatewayClientConfig) GetCNAMERecord(name string, record *BluecatCNAMERecord) error {
   327  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
   328  		"/views/" + c.View + "/" +
   329  		"cname_records/" + name + "/"
   330  
   331  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
   332  	if err != nil {
   333  		return errors.Wrapf(err, "error retrieving cname record %v from gateway", name)
   334  	}
   335  	defer response.Body.Close()
   336  
   337  	if response.StatusCode != http.StatusOK {
   338  		return errors.Errorf("received http %v while retrieving cname record %v from gateway", response.StatusCode, name)
   339  	}
   340  
   341  	json.NewDecoder(response.Body).Decode(record)
   342  	log.Debugf("Get CName Record Response: %v", record)
   343  	return nil
   344  }
   345  
   346  func (c GatewayClientConfig) GetTXTRecord(name string, record *BluecatTXTRecord) error {
   347  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
   348  		"/views/" + c.View + "/" +
   349  		"text_records/" + name + "/"
   350  
   351  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie)
   352  	if err != nil {
   353  		return errors.Wrapf(err, "error retrieving record %v from gateway", name)
   354  	}
   355  	defer response.Body.Close()
   356  
   357  	if response.StatusCode != http.StatusOK {
   358  		return errors.Errorf("received http %v while retrieving txt record %v from gateway", response.StatusCode, name)
   359  	}
   360  
   361  	json.NewDecoder(response.Body).Decode(record)
   362  	log.Debugf("Get TXT Record Response: %v", record)
   363  
   364  	return nil
   365  }
   366  
   367  func (c GatewayClientConfig) CreateHostRecord(zone string, req *BluecatCreateHostRecordRequest) error {
   368  	zonePath := expandZone(zone)
   369  	// Remove the trailing 'zones/'
   370  	zonePath = strings.TrimSuffix(zonePath, "zones/")
   371  
   372  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/"
   373  	body, err := json.Marshal(req)
   374  	if err != nil {
   375  		return errors.Wrap(err, "could not marshal body for create host record")
   376  	}
   377  
   378  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie)
   379  	if err != nil {
   380  		return errors.Wrapf(err, "error creating host record %v in gateway", req.AbsoluteName)
   381  	}
   382  	defer response.Body.Close()
   383  
   384  	if response.StatusCode != http.StatusCreated {
   385  		return errors.Errorf("received http %v while creating host record %v in gateway", response.StatusCode, req.AbsoluteName)
   386  	}
   387  
   388  	return nil
   389  }
   390  
   391  func (c GatewayClientConfig) CreateCNAMERecord(zone string, req *BluecatCreateCNAMERecordRequest) error {
   392  	zonePath := expandZone(zone)
   393  	// Remove the trailing 'zones/'
   394  	zonePath = strings.TrimSuffix(zonePath, "zones/")
   395  
   396  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/"
   397  	body, err := json.Marshal(req)
   398  	if err != nil {
   399  		return errors.Wrap(err, "could not marshal body for create cname record")
   400  	}
   401  
   402  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie)
   403  	if err != nil {
   404  		return errors.Wrapf(err, "error creating cname record %v in gateway", req.AbsoluteName)
   405  	}
   406  	defer response.Body.Close()
   407  
   408  	if response.StatusCode != http.StatusCreated {
   409  		return errors.Errorf("received http %v while creating cname record %v to alias %v in gateway", response.StatusCode, req.AbsoluteName, req.LinkedRecord)
   410  	}
   411  
   412  	return nil
   413  }
   414  
   415  func (c GatewayClientConfig) CreateTXTRecord(zone string, req *BluecatCreateTXTRecordRequest) error {
   416  	zonePath := expandZone(zone)
   417  	// Remove the trailing 'zones/'
   418  	zonePath = strings.TrimSuffix(zonePath, "zones/")
   419  
   420  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/"
   421  	body, err := json.Marshal(req)
   422  	if err != nil {
   423  		return errors.Wrap(err, "could not marshal body for create txt record")
   424  	}
   425  
   426  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie)
   427  	if err != nil {
   428  		return errors.Wrapf(err, "error creating txt record %v in gateway", req.AbsoluteName)
   429  	}
   430  	defer response.Body.Close()
   431  
   432  	if response.StatusCode != http.StatusCreated {
   433  		return errors.Errorf("received http %v while creating txt record %v in gateway", response.StatusCode, req.AbsoluteName)
   434  	}
   435  
   436  	return nil
   437  }
   438  
   439  func (c GatewayClientConfig) DeleteHostRecord(name string, zone string) (err error) {
   440  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
   441  		"/views/" + c.View + "/" +
   442  		"host_records/" + name + "." + zone + "/"
   443  
   444  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodDelete, url, c.Token, nil, c.Cookie)
   445  	if err != nil {
   446  		return errors.Wrapf(err, "error deleting host record %v from gateway", name)
   447  	}
   448  
   449  	if response.StatusCode != http.StatusNoContent {
   450  		return errors.Errorf("received http %v while deleting host record %v from gateway", response.StatusCode, name)
   451  	}
   452  
   453  	return nil
   454  }
   455  
   456  func (c GatewayClientConfig) DeleteCNAMERecord(name string, zone string) (err error) {
   457  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
   458  		"/views/" + c.View + "/" +
   459  		"cname_records/" + name + "." + zone + "/"
   460  
   461  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodDelete, url, c.Token, nil, c.Cookie)
   462  	if err != nil {
   463  		return errors.Wrapf(err, "error deleting cname record %v from gateway", name)
   464  	}
   465  	if response.StatusCode != http.StatusNoContent {
   466  		return errors.Errorf("received http %v while deleting cname record %v from gateway", response.StatusCode, name)
   467  	}
   468  
   469  	return nil
   470  }
   471  
   472  func (c GatewayClientConfig) DeleteTXTRecord(name string, zone string) error {
   473  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
   474  		"/views/" + c.View + "/" +
   475  		"text_records/" + name + "." + zone + "/"
   476  
   477  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodDelete, url, c.Token, nil, c.Cookie)
   478  	if err != nil {
   479  		return errors.Wrapf(err, "error deleting txt record %v from gateway", name)
   480  	}
   481  	if response.StatusCode != http.StatusNoContent {
   482  		return errors.Errorf("received http %v while deleting txt record %v from gateway", response.StatusCode, name)
   483  	}
   484  
   485  	return nil
   486  }
   487  
   488  func (c GatewayClientConfig) ServerFullDeploy() error {
   489  	log.Infof("Executing full deploy on server %s", c.DNSServerName)
   490  	url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/server/full_deploy/"
   491  	requestBody := BluecatServerFullDeployRequest{
   492  		ServerName: c.DNSServerName,
   493  	}
   494  
   495  	body, err := json.Marshal(requestBody)
   496  	if err != nil {
   497  		return errors.Wrap(err, "could not marshal body for server full deploy")
   498  	}
   499  
   500  	response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie)
   501  	if err != nil {
   502  		return errors.Wrap(err, "error executing full deploy")
   503  	}
   504  
   505  	if response.StatusCode != http.StatusCreated {
   506  		responseBody, err := io.ReadAll(response.Body)
   507  		if err != nil {
   508  			return errors.Wrap(err, "failed to read full deploy response body")
   509  		}
   510  		return errors.Errorf("got HTTP response code %v, detailed message: %v", response.StatusCode, string(responseBody))
   511  	}
   512  
   513  	return nil
   514  }
   515  
   516  // SplitProperties is a helper function to break a '|' separated string into key/value pairs
   517  // i.e. "foo=bar|baz=mop"
   518  func SplitProperties(props string) map[string]string {
   519  	propMap := make(map[string]string)
   520  	// remove trailing | character before we split
   521  	props = strings.TrimSuffix(props, "|")
   522  
   523  	splits := strings.Split(props, "|")
   524  	for _, pair := range splits {
   525  		items := strings.Split(pair, "=")
   526  		propMap[items[0]] = items[1]
   527  	}
   528  
   529  	return propMap
   530  }
   531  
   532  // IsValidDNSDeployType validates the deployment type provided by a users configuration is supported by the Bluecat Provider.
   533  func IsValidDNSDeployType(deployType string) bool {
   534  	validDNSDeployTypes := []string{"no-deploy", "full-deploy"}
   535  	for _, t := range validDNSDeployTypes {
   536  		if t == deployType {
   537  			return true
   538  		}
   539  	}
   540  	return false
   541  }
   542  
   543  // expandZone takes an absolute domain name such as 'example.com' and returns a zone hierarchy used by Bluecat Gateway,
   544  // such as '/zones/com/zones/example/zones/'
   545  func expandZone(zone string) string {
   546  	ze := "zones/"
   547  	parts := strings.Split(zone, ".")
   548  	if len(parts) > 1 {
   549  		last := len(parts) - 1
   550  		for i := range parts {
   551  			ze = ze + parts[last-i] + "/zones/"
   552  		}
   553  	} else {
   554  		ze = ze + zone + "/zones/"
   555  	}
   556  	return ze
   557  }
   558  
   559  func executeHTTPRequest(skipTLSVerify bool, method, url, token string, body io.Reader, cookie http.Cookie) (*http.Response, error) {
   560  	httpClient := &http.Client{
   561  		Transport: &http.Transport{
   562  			Proxy: http.ProxyFromEnvironment,
   563  			TLSClientConfig: &tls.Config{
   564  				InsecureSkipVerify: skipTLSVerify,
   565  			},
   566  		},
   567  	}
   568  	request, err := http.NewRequest(method, url, body)
   569  	if err != nil {
   570  		return nil, err
   571  	}
   572  	if request.Method == http.MethodPost {
   573  		request.Header.Add("Content-Type", "application/json")
   574  	}
   575  	request.Header.Add("Accept", "application/json")
   576  
   577  	if token != "" {
   578  		request.Header.Add("Authorization", "Basic "+token)
   579  	}
   580  	request.AddCookie(&cookie)
   581  
   582  	return httpClient.Do(request)
   583  }