From 83f062fbf77634fa86f201e07c1557d5aa639bdc Mon Sep 17 00:00:00 2001 From: Vinny Sabatini Date: Tue, 1 Mar 2022 21:41:04 -0600 Subject: [PATCH 1/8] provider/bluecat: migrate client code to separate pkg Extract all Bluecat Gateway Client code to separate package --- provider/bluecat/bluecat.go | 665 ++------------------------- provider/bluecat/bluecat_test.go | 134 +++--- provider/bluecat/gateway/api.go | 608 ++++++++++++++++++++++++ provider/bluecat/gateway/api_test.go | 43 ++ 4 files changed, 750 insertions(+), 700 deletions(-) create mode 100644 provider/bluecat/gateway/api.go create mode 100644 provider/bluecat/gateway/api_test.go diff --git a/provider/bluecat/bluecat.go b/provider/bluecat/bluecat.go index e54a84e65..06ef10e5f 100644 --- a/provider/bluecat/bluecat.go +++ b/provider/bluecat/bluecat.go @@ -15,17 +15,16 @@ limitations under the License. */ // TODO: Ensure we have proper error handling/logging for API calls to Bluecat. getBluecatGatewayToken has a good example of this +// TODO: Remove naked returns +// TODO: Remove studdering +// TODO: Make API calls more consistent (eg error handling on HTTP response codes) +// TODO: zone-id-filter does not seem to work with our provider package bluecat import ( - "bytes" "context" - "crypto/tls" "encoding/json" - "io" - "io/ioutil" - "net/http" "os" "regexp" "strconv" @@ -37,20 +36,9 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" + api "sigs.k8s.io/external-dns/provider/bluecat/gateway" ) -type bluecatConfig struct { - GatewayHost string `json:"gatewayHost"` - GatewayUsername string `json:"gatewayUsername,omitempty"` - GatewayPassword string `json:"gatewayPassword,omitempty"` - DNSConfiguration string `json:"dnsConfiguration"` - DNSServerName string `json:"dnsServerName"` - DNSDeployType string `json:"dnsDeployType"` - View string `json:"dnsView"` - RootZone string `json:"rootZone"` - SkipTLSVerify bool `json:"skipTLSVerify"` -} - // BluecatProvider implements the DNS provider for Bluecat DNS type BluecatProvider struct { provider.BaseProvider @@ -62,68 +50,7 @@ type BluecatProvider struct { DNSServerName string DNSDeployType string View string - gatewayClient GatewayClient -} - -type GatewayClient interface { - getBluecatZones(zoneName string) ([]BluecatZone, error) - getHostRecords(zone string, records *[]BluecatHostRecord) error - getCNAMERecords(zone string, records *[]BluecatCNAMERecord) error - getHostRecord(name string, record *BluecatHostRecord) error - getCNAMERecord(name string, record *BluecatCNAMERecord) error - createHostRecord(zone string, req *bluecatCreateHostRecordRequest) (res interface{}, err error) - createCNAMERecord(zone string, req *bluecatCreateCNAMERecordRequest) (res interface{}, err error) - deleteHostRecord(name string, zone string) (err error) - deleteCNAMERecord(name string, zone string) (err error) - buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error) - getTXTRecords(zone string, records *[]BluecatTXTRecord) error - getTXTRecord(name string, record *BluecatTXTRecord) error - createTXTRecord(zone string, req *bluecatCreateTXTRecordRequest) (res interface{}, err error) - deleteTXTRecord(name string, zone string) error - serverFullDeploy() error -} - -// GatewayClientConfig defines new client on bluecat gateway -type GatewayClientConfig struct { - Cookie http.Cookie - Token string - Host string - DNSConfiguration string - View string - RootZone string - DNSServerName string - SkipTLSVerify bool -} - -// BluecatZone defines a zone to hold records -type BluecatZone struct { - ID int `json:"id"` - Name string `json:"name"` - Properties string `json:"properties"` - Type string `json:"type"` -} - -// BluecatHostRecord defines dns Host record -type BluecatHostRecord struct { - ID int `json:"id"` - Name string `json:"name"` - Properties string `json:"properties"` - Type string `json:"type"` -} - -// BluecatCNAMERecord defines dns CNAME record -type BluecatCNAMERecord struct { - ID int `json:"id"` - Name string `json:"name"` - Properties string `json:"properties"` - Type string `json:"type"` -} - -// BluecatTXTRecord defines dns TXT record -type BluecatTXTRecord struct { - ID int `json:"id"` - Name string `json:"name"` - Properties string `json:"properties"` + gatewayClient api.GatewayClient } type bluecatRecordSet struct { @@ -131,38 +58,15 @@ type bluecatRecordSet struct { res interface{} } -type bluecatCreateHostRecordRequest struct { - AbsoluteName string `json:"absolute_name"` - IP4Address string `json:"ip4_address"` - TTL int `json:"ttl"` - Properties string `json:"properties"` -} - -type bluecatCreateCNAMERecordRequest struct { - AbsoluteName string `json:"absolute_name"` - LinkedRecord string `json:"linked_record"` - TTL int `json:"ttl"` - Properties string `json:"properties"` -} - -type bluecatCreateTXTRecordRequest struct { - AbsoluteName string `json:"absolute_name"` - Text string `json:"txt"` -} - -type bluecatServerFullDeployRequest struct { - ServerName string `json:"server_name"` -} - // NewBluecatProvider creates a new Bluecat provider. // // Returns a pointer to the provider or an error if a provider could not be created. func NewBluecatProvider(configFile, dnsConfiguration, dnsServerName, dnsDeployType, dnsView, gatewayHost, rootZone string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun, skipTLSVerify bool) (*BluecatProvider, error) { - cfg := bluecatConfig{} + cfg := api.BluecatConfig{} contents, err := os.ReadFile(configFile) if err != nil { if errors.Is(err, os.ErrNotExist) { - cfg = bluecatConfig{ + cfg = api.BluecatConfig{ GatewayHost: gatewayHost, DNSConfiguration: dnsConfiguration, DNSServerName: dnsServerName, @@ -183,15 +87,15 @@ func NewBluecatProvider(configFile, dnsConfiguration, dnsServerName, dnsDeployTy } } - if !isValidDNSDeployType(cfg.DNSDeployType) { + if !api.IsValidDNSDeployType(cfg.DNSDeployType) { return nil, errors.Errorf("%v is not a valid deployment type", cfg.DNSDeployType) } - token, cookie, err := getBluecatGatewayToken(cfg) + token, cookie, err := api.GetBluecatGatewayToken(cfg) if err != nil { return nil, errors.Wrap(err, "failed to get API token from Bluecat Gateway") } - gatewayClient := NewGatewayClient(cookie, token, cfg.GatewayHost, cfg.DNSConfiguration, cfg.View, cfg.RootZone, cfg.DNSServerName, cfg.SkipTLSVerify) + gatewayClient := api.NewGatewayClientConfig(cookie, token, cfg.GatewayHost, cfg.DNSConfiguration, cfg.View, cfg.RootZone, cfg.DNSServerName, cfg.SkipTLSVerify) provider := &BluecatProvider{ domainFilter: domainFilter, @@ -207,27 +111,6 @@ func NewBluecatProvider(configFile, dnsConfiguration, dnsServerName, dnsDeployTy return provider, nil } -// NewGatewayClient creates and returns a new Bluecat gateway client -func NewGatewayClient(cookie http.Cookie, token, gatewayHost, dnsConfiguration, view, rootZone, dnsServerName string, skipTLSVerify bool) GatewayClientConfig { - // TODO: do not handle defaulting here - // - // Right now the Bluecat gateway doesn't seem to have a way to get the root zone from the API. If the user - // doesn't provide one via the config file we'll assume it's 'com' - if rootZone == "" { - rootZone = "com" - } - return GatewayClientConfig{ - Cookie: cookie, - Token: token, - Host: gatewayHost, - DNSConfiguration: dnsConfiguration, - DNSServerName: dnsServerName, - View: view, - RootZone: rootZone, - SkipTLSVerify: skipTLSVerify, - } -} - // Records fetches Host, CNAME, and TXT records from bluecat gateway func (p *BluecatProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) { zones, err := p.zones() @@ -239,8 +122,8 @@ func (p *BluecatProvider) Records(ctx context.Context) (endpoints []*endpoint.En for _, zone := range zones { log.Debugf("fetching records from zone '%s'", zone) - var resT []BluecatTXTRecord - err = p.gatewayClient.getTXTRecords(zone, &resT) + var resT []api.BluecatTXTRecord + err = p.gatewayClient.GetTXTRecords(zone, &resT) if err != nil { return nil, errors.Wrapf(err, "could not fetch TXT records for zone: %v", zone) } @@ -253,14 +136,14 @@ func (p *BluecatProvider) Records(ctx context.Context) (endpoints []*endpoint.En endpoints = append(endpoints, tempEndpoint) } - var resH []BluecatHostRecord - err = p.gatewayClient.getHostRecords(zone, &resH) + var resH []api.BluecatHostRecord + err = p.gatewayClient.GetHostRecords(zone, &resH) if err != nil { return nil, errors.Wrapf(err, "could not fetch host records for zone: %v", zone) } var ep *endpoint.Endpoint for _, rec := range resH { - propMap := splitProperties(rec.Properties) + propMap := api.SplitProperties(rec.Properties) ips := strings.Split(propMap["addresses"], ",") for _, ip := range ips { if _, ok := propMap["ttl"]; ok { @@ -284,14 +167,14 @@ func (p *BluecatProvider) Records(ctx context.Context) (endpoints []*endpoint.En } } - var resC []BluecatCNAMERecord - err = p.gatewayClient.getCNAMERecords(zone, &resC) + var resC []api.BluecatCNAMERecord + err = p.gatewayClient.GetCNAMERecords(zone, &resC) if err != nil { return nil, errors.Wrapf(err, "could not fetch CNAME records for zone: %v", zone) } for _, rec := range resC { - propMap := splitProperties(rec.Properties) + propMap := api.SplitProperties(rec.Properties) if _, ok := propMap["ttl"]; ok { ttl, err := strconv.Atoi(propMap["ttl"]) if err != nil { @@ -336,7 +219,7 @@ func (p *BluecatProvider) ApplyChanges(ctx context.Context, changes *plan.Change if p.DNSServerName != "" { switch p.DNSDeployType { case "full-deploy": - err := p.gatewayClient.serverFullDeploy() + err := p.gatewayClient.ServerFullDeploy() if err != nil { return err } @@ -404,7 +287,7 @@ func (p *BluecatProvider) zones() ([]string, error) { log.Debugf("retrieving Bluecat zones for configuration: %s, view: %s", p.DNSConfiguration, p.View) var zones []string - zonelist, err := p.gatewayClient.getBluecatZones(p.RootZone) + zonelist, err := p.gatewayClient.GetBluecatZones(p.RootZone) if err != nil { return nil, err } @@ -419,7 +302,7 @@ func (p *BluecatProvider) zones() ([]string, error) { continue } - zoneProps := splitProperties(zone.Properties) + zoneProps := api.SplitProperties(zone.Properties) zones = append(zones, zoneProps["absoluteName"]) } @@ -462,11 +345,11 @@ func (p *BluecatProvider) createRecords(created bluecatChangeMap) { var response interface{} switch ep.RecordType { case endpoint.RecordTypeA: - response, err = p.gatewayClient.createHostRecord(zone, recordSet.obj.(*bluecatCreateHostRecordRequest)) + response, err = p.gatewayClient.CreateHostRecord(zone, recordSet.obj.(*api.BluecatCreateHostRecordRequest)) case endpoint.RecordTypeCNAME: - response, err = p.gatewayClient.createCNAMERecord(zone, recordSet.obj.(*bluecatCreateCNAMERecordRequest)) + response, err = p.gatewayClient.CreateCNAMERecord(zone, recordSet.obj.(*api.BluecatCreateCNAMERecordRequest)) case endpoint.RecordTypeTXT: - response, err = p.gatewayClient.createTXTRecord(zone, recordSet.obj.(*bluecatCreateTXTRecordRequest)) + response, err = p.gatewayClient.CreateTXTRecord(zone, recordSet.obj.(*api.BluecatCreateTXTRecordRequest)) } log.Debugf("Response from create: %v", response) if err != nil { @@ -516,16 +399,16 @@ func (p *BluecatProvider) deleteRecords(deleted bluecatChangeMap) { switch ep.RecordType { case endpoint.RecordTypeA: - for _, record := range *recordSet.res.(*[]BluecatHostRecord) { - err = p.gatewayClient.deleteHostRecord(record.Name, zone) + for _, record := range *recordSet.res.(*[]api.BluecatHostRecord) { + err = p.gatewayClient.DeleteHostRecord(record.Name, zone) } case endpoint.RecordTypeCNAME: - for _, record := range *recordSet.res.(*[]BluecatCNAMERecord) { - err = p.gatewayClient.deleteCNAMERecord(record.Name, zone) + for _, record := range *recordSet.res.(*[]api.BluecatCNAMERecord) { + err = p.gatewayClient.DeleteCNAMERecord(record.Name, zone) } case endpoint.RecordTypeTXT: - for _, record := range *recordSet.res.(*[]BluecatTXTRecord) { - err = p.gatewayClient.deleteTXTRecord(record.Name, zone) + for _, record := range *recordSet.res.(*[]api.BluecatTXTRecord) { + err = p.gatewayClient.DeleteTXTRecord(record.Name, zone) } } if err != nil { @@ -543,16 +426,16 @@ func (p *BluecatProvider) deleteRecords(deleted bluecatChangeMap) { func (p *BluecatProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (recordSet bluecatRecordSet, err error) { switch ep.RecordType { case endpoint.RecordTypeA: - var res []BluecatHostRecord - obj := bluecatCreateHostRecordRequest{ + var res []api.BluecatHostRecord + obj := api.BluecatCreateHostRecordRequest{ AbsoluteName: ep.DNSName, IP4Address: ep.Targets[0], TTL: int(ep.RecordTTL), Properties: "", } if getObject { - var record BluecatHostRecord - err = p.gatewayClient.getHostRecord(ep.DNSName, &record) + var record api.BluecatHostRecord + err = p.gatewayClient.GetHostRecord(ep.DNSName, &record) if err != nil { return } @@ -563,16 +446,16 @@ func (p *BluecatProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (reco res: &res, } case endpoint.RecordTypeCNAME: - var res []BluecatCNAMERecord - obj := bluecatCreateCNAMERecordRequest{ + var res []api.BluecatCNAMERecord + obj := api.BluecatCreateCNAMERecordRequest{ AbsoluteName: ep.DNSName, LinkedRecord: ep.Targets[0], TTL: int(ep.RecordTTL), Properties: "", } if getObject { - var record BluecatCNAMERecord - err = p.gatewayClient.getCNAMERecord(ep.DNSName, &record) + var record api.BluecatCNAMERecord + err = p.gatewayClient.GetCNAMERecord(ep.DNSName, &record) if err != nil { return } @@ -583,16 +466,16 @@ func (p *BluecatProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (reco res: &res, } case endpoint.RecordTypeTXT: - var res []BluecatTXTRecord + var res []api.BluecatTXTRecord // TODO: Allow setting TTL // This is not implemented in the Bluecat Gateway - obj := bluecatCreateTXTRecordRequest{ + obj := api.BluecatCreateTXTRecordRequest{ AbsoluteName: ep.DNSName, Text: ep.Targets[0], } if getObject { - var record BluecatTXTRecord - err = p.gatewayClient.getTXTRecord(ep.DNSName, &record) + var record api.BluecatTXTRecord + err = p.gatewayClient.GetTXTRecord(ep.DNSName, &record) if err != nil { return } @@ -606,455 +489,7 @@ func (p *BluecatProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (reco return } -// getBluecatGatewayToken retrieves a Bluecat Gateway API token. -func getBluecatGatewayToken(cfg bluecatConfig) (string, http.Cookie, error) { - var username string - if cfg.GatewayUsername != "" { - username = cfg.GatewayUsername - } - if v, ok := os.LookupEnv("BLUECAT_USERNAME"); ok { - username = v - } - - var password string - if cfg.GatewayPassword != "" { - password = cfg.GatewayPassword - } - if v, ok := os.LookupEnv("BLUECAT_PASSWORD"); ok { - password = v - } - - body, err := json.Marshal(map[string]string{ - "username": username, - "password": password, - }) - if err != nil { - return "", http.Cookie{}, errors.Wrap(err, "could not unmarshal credentials for bluecat gateway config") - } - - c := newHTTPClient(cfg.SkipTLSVerify) - - resp, err := c.Post(cfg.GatewayHost+"/rest_login", "application/json", bytes.NewBuffer(body)) - if err != nil { - return "", http.Cookie{}, errors.Wrap(err, "error obtaining API token from bluecat gateway") - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - details, _ := ioutil.ReadAll(resp.Body) - return "", http.Cookie{}, errors.Errorf("got HTTP response code %v, detailed message: %v", resp.StatusCode, string(details)) - } - - res, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", http.Cookie{}, errors.Wrap(err, "error reading get_token response from bluecat gateway") - } - - resJSON := map[string]string{} - err = json.Unmarshal(res, &resJSON) - if err != nil { - return "", http.Cookie{}, errors.Wrap(err, "error unmarshaling json response (auth) from bluecat gateway") - } - - // Example response: {"access_token": "BAMAuthToken: abc123"} - // We only care about the actual token string - i.e. abc123 - // The gateway also creates a cookie as part of the response. This seems to be the actual auth mechanism, at least - // for now. - return strings.Split(resJSON["access_token"], " ")[1], *resp.Cookies()[0], nil -} - -func (c GatewayClientConfig) getBluecatZones(zoneName string) ([]BluecatZone, error) { - client := newHTTPClient(c.SkipTLSVerify) - - zonePath := expandZone(zoneName) - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return nil, errors.Wrap(err, "error building http request") - } - - resp, err := client.Do(req) - if err != nil { - return nil, errors.Wrapf(err, "error retrieving zone(s) from gateway: %v, %v", url, zoneName) - } - - defer resp.Body.Close() - - zones := []BluecatZone{} - json.NewDecoder(resp.Body).Decode(&zones) - - // Bluecat Gateway only returns subzones one level deeper than the provided zone - // so this recursion is needed to traverse subzones until none are returned - for _, zone := range zones { - zoneProps := splitProperties(zone.Properties) - subZones, err := c.getBluecatZones(zoneProps["absoluteName"]) - if err != nil { - return nil, errors.Wrapf(err, "error retrieving subzones from gateway: %v", zoneName) - } - zones = append(zones, subZones...) - } - - return zones, nil -} - -func (c GatewayClientConfig) getHostRecords(zone string, records *[]BluecatHostRecord) error { - client := newHTTPClient(c.SkipTLSVerify) - - zonePath := expandZone(zone) - - // Remove the trailing 'zones/' - zonePath = strings.TrimSuffix(zonePath, "zones/") - - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/" - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return errors.Wrap(err, "error building http request") - } - - resp, err := client.Do(req) - if err != nil { - return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone) - } - - defer resp.Body.Close() - - json.NewDecoder(resp.Body).Decode(records) - log.Debugf("Get Host Records Response: %v", records) - - return nil -} - -func (c GatewayClientConfig) getCNAMERecords(zone string, records *[]BluecatCNAMERecord) error { - client := newHTTPClient(c.SkipTLSVerify) - - zonePath := expandZone(zone) - - // Remove the trailing 'zones/' - zonePath = strings.TrimSuffix(zonePath, "zones/") - - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/" - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return errors.Wrap(err, "error building http request") - } - - resp, err := client.Do(req) - if err != nil { - return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone) - } - - defer resp.Body.Close() - - json.NewDecoder(resp.Body).Decode(records) - log.Debugf("Get CName Records Response: %v", records) - - return nil -} - -func (c GatewayClientConfig) getTXTRecords(zone string, records *[]BluecatTXTRecord) error { - client := newHTTPClient(c.SkipTLSVerify) - - zonePath := expandZone(zone) - - // Remove the trailing 'zones/' - zonePath = strings.TrimSuffix(zonePath, "zones/") - - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/" - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return errors.Wrap(err, "error building http request") - } - log.Debugf("Request: %v", req) - - resp, err := client.Do(req) - if err != nil { - return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone) - } - log.Debugf("Get Txt Records response: %v", resp) - - defer resp.Body.Close() - json.NewDecoder(resp.Body).Decode(records) - log.Debugf("Get TXT Records Body: %v", records) - - return nil -} - -func (c GatewayClientConfig) getHostRecord(name string, record *BluecatHostRecord) error { - client := newHTTPClient(c.SkipTLSVerify) - - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + - "/views/" + c.View + "/" + - "host_records/" + name + "/" - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return errors.Wrapf(err, "error building http request: %v", name) - } - - resp, err := client.Do(req) - if err != nil { - return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name) - } - - defer resp.Body.Close() - - json.NewDecoder(resp.Body).Decode(record) - log.Debugf("Get Host Record Response: %v", record) - return nil -} - -func (c GatewayClientConfig) getCNAMERecord(name string, record *BluecatCNAMERecord) error { - client := newHTTPClient(c.SkipTLSVerify) - - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + - "/views/" + c.View + "/" + - "cname_records/" + name + "/" - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return errors.Wrapf(err, "error building http request: %v", name) - } - - resp, err := client.Do(req) - if err != nil { - return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name) - } - - defer resp.Body.Close() - - json.NewDecoder(resp.Body).Decode(record) - log.Debugf("Get CName Record Response: %v", record) - return nil -} - -func (c GatewayClientConfig) getTXTRecord(name string, record *BluecatTXTRecord) error { - client := newHTTPClient(c.SkipTLSVerify) - - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + - "/views/" + c.View + "/" + - "text_records/" + name + "/" - - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return errors.Wrap(err, "error building http request") - } - - resp, err := client.Do(req) - if err != nil { - return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name) - } - - defer resp.Body.Close() - json.NewDecoder(resp.Body).Decode(record) - log.Debugf("Get TXT Record Response: %v", record) - - return nil -} - -func (c GatewayClientConfig) createHostRecord(zone string, req *bluecatCreateHostRecordRequest) (res interface{}, err error) { - client := newHTTPClient(c.SkipTLSVerify) - - zonePath := expandZone(zone) - // Remove the trailing 'zones/' - zonePath = strings.TrimSuffix(zonePath, "zones/") - - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/" - body, _ := json.Marshal(req) - hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) - if err != nil { - return nil, errors.Wrap(err, "error building http request") - } - hreq.Header.Add("Content-Type", "application/json") - res, err = client.Do(hreq) - - return -} - -func (c GatewayClientConfig) createCNAMERecord(zone string, req *bluecatCreateCNAMERecordRequest) (res interface{}, err error) { - client := newHTTPClient(c.SkipTLSVerify) - - zonePath := expandZone(zone) - // Remove the trailing 'zones/' - zonePath = strings.TrimSuffix(zonePath, "zones/") - - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/" - body, _ := json.Marshal(req) - - hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) - if err != nil { - return nil, errors.Wrap(err, "error building http request") - } - - hreq.Header.Add("Content-Type", "application/json") - res, err = client.Do(hreq) - - return -} - -func (c GatewayClientConfig) createTXTRecord(zone string, req *bluecatCreateTXTRecordRequest) (interface{}, error) { - client := newHTTPClient(c.SkipTLSVerify) - - zonePath := expandZone(zone) - // Remove the trailing 'zones/' - zonePath = strings.TrimSuffix(zonePath, "zones/") - - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/" - body, _ := json.Marshal(req) - hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) - if err != nil { - return nil, err - } - - hreq.Header.Add("Content-Type", "application/json") - res, err := client.Do(hreq) - - return res, err -} - -func (c GatewayClientConfig) deleteHostRecord(name string, zone string) (err error) { - client := newHTTPClient(c.SkipTLSVerify) - - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + - "/views/" + c.View + "/" + - "host_records/" + name + "." + zone + "/" - req, err := c.buildHTTPRequest("DELETE", url, nil) - if err != nil { - return errors.Wrapf(err, "error building http request: %v", name) - } - - _, err = client.Do(req) - if err != nil { - return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name) - } - - return nil -} - -func (c GatewayClientConfig) deleteCNAMERecord(name string, zone string) (err error) { - client := newHTTPClient(c.SkipTLSVerify) - - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + - "/views/" + c.View + "/" + - "cname_records/" + name + "." + zone + "/" - req, err := c.buildHTTPRequest("DELETE", url, nil) - if err != nil { - return errors.Wrapf(err, "error building http request: %v", name) - } - - _, err = client.Do(req) - if err != nil { - return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name) - } - - return nil -} - -func (c GatewayClientConfig) deleteTXTRecord(name string, zone string) error { - client := newHTTPClient(c.SkipTLSVerify) - - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + - "/views/" + c.View + "/" + - "text_records/" + name + "." + zone + "/" - - req, err := c.buildHTTPRequest("DELETE", url, nil) - if err != nil { - return errors.Wrap(err, "error building http request") - } - - _, err = client.Do(req) - if err != nil { - return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name) - } - - return nil -} - -func (c GatewayClientConfig) serverFullDeploy() error { - log.Infof("Executing full deploy on server %s", c.DNSServerName) - httpClient := newHTTPClient(c.SkipTLSVerify) - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/server/full_deploy/" - requestBody := bluecatServerFullDeployRequest{ - ServerName: c.DNSServerName, - } - - body, err := json.Marshal(requestBody) - if err != nil { - return errors.Wrap(err, "could not marshal body for server full deploy") - } - - request, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) - if err != nil { - return errors.Wrap(err, "error building http request") - } - - request.Header.Add("Content-Type", "application/json") - response, err := httpClient.Do(request) - if err != nil { - return errors.Wrap(err, "error executing full deploy") - } - - if response.StatusCode != http.StatusCreated { - responseBody, err := ioutil.ReadAll(response.Body) - if err != nil { - return errors.Wrap(err, "failed to read full deploy response body") - } - return errors.Errorf("got HTTP response code %v, detailed message: %v", response.StatusCode, string(responseBody)) - } - - return nil -} - -//buildHTTPRequest builds a standard http Request and adds authentication headers required by Bluecat Gateway -func (c GatewayClientConfig) buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error) { - req, err := http.NewRequest(method, url, body) - req.Header.Add("Accept", "application/json") - req.Header.Add("Authorization", "Basic "+c.Token) - req.AddCookie(&c.Cookie) - return req, err -} - -//splitProperties is a helper function to break a '|' separated string into key/value pairs -// i.e. "foo=bar|baz=mop" -func splitProperties(props string) map[string]string { - propMap := make(map[string]string) - // remove trailing | character before we split - props = strings.TrimSuffix(props, "|") - - splits := strings.Split(props, "|") - for _, pair := range splits { - items := strings.Split(pair, "=") - propMap[items[0]] = items[1] - } - - return propMap -} - -// isValidDNSDeployType validates the deployment type provided by a users configuration is supported by the Bluecat Provider. -func isValidDNSDeployType(deployType string) bool { - validDNSDeployTypes := []string{"no-deploy", "full-deploy"} - for _, t := range validDNSDeployTypes { - if t == deployType { - return true - } - } - return false -} - -//expandZone takes an absolute domain name such as 'example.com' and returns a zone hierarchy used by Bluecat Gateway, -//such as '/zones/com/zones/example/zones/' -func expandZone(zone string) string { - ze := "zones/" - parts := strings.Split(zone, ".") - if len(parts) > 1 { - last := len(parts) - 1 - for i := range parts { - ze = ze + parts[last-i] + "/zones/" - } - } else { - ze = ze + zone + "/zones/" - } - return ze -} - -//extractOwnerFromTXTRecord takes a single text property string and returns the owner after parsing theowner string. +// extractOwnerFromTXTRecord takes a single text property string and returns the owner after parsing the owner string. func extractOwnerfromTXTRecord(propString string) (string, error) { if len(propString) == 0 { return "", errors.Errorf("External-DNS Owner not found") @@ -1066,15 +501,3 @@ func extractOwnerfromTXTRecord(propString string) (string, error) { } return strings.Split(match[0], "=")[1], nil } - -// newHTTPClient returns an instance of http client -func newHTTPClient(skipTLSVerify bool) *http.Client { - return &http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: skipTLSVerify, - }, - }, - } -} diff --git a/provider/bluecat/bluecat_test.go b/provider/bluecat/bluecat_test.go index 17f541265..13ee8f762 100644 --- a/provider/bluecat/bluecat_test.go +++ b/provider/bluecat/bluecat_test.go @@ -26,13 +26,14 @@ import ( "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" + api "sigs.k8s.io/external-dns/provider/bluecat/gateway" ) type mockGatewayClient struct { - mockBluecatZones *[]BluecatZone - mockBluecatHosts *[]BluecatHostRecord - mockBluecatCNAMEs *[]BluecatCNAMERecord - mockBluecatTXTs *[]BluecatTXTRecord + mockBluecatZones *[]api.BluecatZone + mockBluecatHosts *[]api.BluecatHostRecord + mockBluecatCNAMEs *[]api.BluecatCNAMERecord + mockBluecatTXTs *[]api.BluecatTXTRecord } type Changes struct { @@ -46,18 +47,18 @@ type Changes struct { Delete []*endpoint.Endpoint } -func (g mockGatewayClient) getBluecatZones(zoneName string) ([]BluecatZone, error) { +func (g mockGatewayClient) GetBluecatZones(zoneName string) ([]api.BluecatZone, error) { return *g.mockBluecatZones, nil } -func (g mockGatewayClient) getHostRecords(zone string, records *[]BluecatHostRecord) error { +func (g mockGatewayClient) GetHostRecords(zone string, records *[]api.BluecatHostRecord) error { *records = *g.mockBluecatHosts return nil } -func (g mockGatewayClient) getCNAMERecords(zone string, records *[]BluecatCNAMERecord) error { +func (g mockGatewayClient) GetCNAMERecords(zone string, records *[]api.BluecatCNAMERecord) error { *records = *g.mockBluecatCNAMEs return nil } -func (g mockGatewayClient) getHostRecord(name string, record *BluecatHostRecord) error { +func (g mockGatewayClient) GetHostRecord(name string, record *api.BluecatHostRecord) error { for _, currentRecord := range *g.mockBluecatHosts { if currentRecord.Name == strings.Split(name, ".")[0] { *record = currentRecord @@ -66,7 +67,7 @@ func (g mockGatewayClient) getHostRecord(name string, record *BluecatHostRecord) } return nil } -func (g mockGatewayClient) getCNAMERecord(name string, record *BluecatCNAMERecord) error { +func (g mockGatewayClient) GetCNAMERecord(name string, record *api.BluecatCNAMERecord) error { for _, currentRecord := range *g.mockBluecatCNAMEs { if currentRecord.Name == strings.Split(name, ".")[0] { *record = currentRecord @@ -75,25 +76,25 @@ func (g mockGatewayClient) getCNAMERecord(name string, record *BluecatCNAMERecor } return nil } -func (g mockGatewayClient) createHostRecord(zone string, req *bluecatCreateHostRecordRequest) (res interface{}, err error) { +func (g mockGatewayClient) CreateHostRecord(zone string, req *api.BluecatCreateHostRecordRequest) (res interface{}, err error) { return nil, nil } -func (g mockGatewayClient) createCNAMERecord(zone string, req *bluecatCreateCNAMERecordRequest) (res interface{}, err error) { +func (g mockGatewayClient) CreateCNAMERecord(zone string, req *api.BluecatCreateCNAMERecordRequest) (res interface{}, err error) { return nil, nil } -func (g mockGatewayClient) deleteHostRecord(name string, zone string) (err error) { +func (g mockGatewayClient) DeleteHostRecord(name string, zone string) (err error) { *g.mockBluecatHosts = nil return nil } -func (g mockGatewayClient) deleteCNAMERecord(name string, zone string) (err error) { +func (g mockGatewayClient) DeleteCNAMERecord(name string, zone string) (err error) { *g.mockBluecatCNAMEs = nil return nil } -func (g mockGatewayClient) getTXTRecords(zone string, records *[]BluecatTXTRecord) error { +func (g mockGatewayClient) GetTXTRecords(zone string, records *[]api.BluecatTXTRecord) error { *records = *g.mockBluecatTXTs return nil } -func (g mockGatewayClient) getTXTRecord(name string, record *BluecatTXTRecord) error { +func (g mockGatewayClient) GetTXTRecord(name string, record *api.BluecatTXTRecord) error { for _, currentRecord := range *g.mockBluecatTXTs { if currentRecord.Name == name { *record = currentRecord @@ -102,14 +103,14 @@ func (g mockGatewayClient) getTXTRecord(name string, record *BluecatTXTRecord) e } return nil } -func (g mockGatewayClient) createTXTRecord(zone string, req *bluecatCreateTXTRecordRequest) (res interface{}, err error) { +func (g mockGatewayClient) CreateTXTRecord(zone string, req *api.BluecatCreateTXTRecordRequest) (res interface{}, err error) { return nil, nil } -func (g mockGatewayClient) deleteTXTRecord(name string, zone string) error { +func (g mockGatewayClient) DeleteTXTRecord(name string, zone string) error { *g.mockBluecatTXTs = nil return nil } -func (g mockGatewayClient) serverFullDeploy() error { +func (g mockGatewayClient) ServerFullDeploy() error { return nil } @@ -118,42 +119,42 @@ func (g mockGatewayClient) buildHTTPRequest(method, url string, body io.Reader) return request, nil } -func createMockBluecatZone(fqdn string) BluecatZone { +func createMockBluecatZone(fqdn string) api.BluecatZone { props := "absoluteName=" + fqdn - return BluecatZone{ + return api.BluecatZone{ Properties: props, Name: fqdn, ID: 3, } } -func createMockBluecatHostRecord(fqdn, target string, ttl int) BluecatHostRecord { +func createMockBluecatHostRecord(fqdn, target string, ttl int) api.BluecatHostRecord { props := "absoluteName=" + fqdn + "|addresses=" + target + "|ttl=" + fmt.Sprint(ttl) + "|" nameParts := strings.Split(fqdn, ".") - return BluecatHostRecord{ + return api.BluecatHostRecord{ Name: nameParts[0], Properties: props, ID: 3, } } -func createMockBluecatCNAME(alias, target string, ttl int) BluecatCNAMERecord { +func createMockBluecatCNAME(alias, target string, ttl int) api.BluecatCNAMERecord { props := "absoluteName=" + alias + "|linkedRecordName=" + target + "|ttl=" + fmt.Sprint(ttl) + "|" nameParts := strings.Split(alias, ".") - return BluecatCNAMERecord{ + return api.BluecatCNAMERecord{ Name: nameParts[0], Properties: props, } } -func createMockBluecatTXT(fqdn, txt string) BluecatTXTRecord { - return BluecatTXTRecord{ +func createMockBluecatTXT(fqdn, txt string) api.BluecatTXTRecord { + return api.BluecatTXTRecord{ Name: fqdn, Properties: txt, } } -func newBluecatProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, client GatewayClient) *BluecatProvider { +func newBluecatProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool, client mockGatewayClient) *BluecatProvider { return &BluecatProvider{ domainFilter: domainFilter, zoneIDFilter: zoneIDFilter, @@ -219,20 +220,20 @@ var tests = bluecatTestData{ func TestBluecatRecords(t *testing.T) { client := mockGatewayClient{ - mockBluecatZones: &[]BluecatZone{ + mockBluecatZones: &[]api.BluecatZone{ createMockBluecatZone("example.com"), }, - mockBluecatTXTs: &[]BluecatTXTRecord{ + mockBluecatTXTs: &[]api.BluecatTXTRecord{ createMockBluecatTXT("kdb.example.com", "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"), createMockBluecatTXT("wack.example.com", "hello"), createMockBluecatTXT("sack.example.com", ""), }, - mockBluecatHosts: &[]BluecatHostRecord{ + mockBluecatHosts: &[]api.BluecatHostRecord{ createMockBluecatHostRecord("example.com", "123.123.123.122", 30), createMockBluecatHostRecord("nginx.example.com", "123.123.123.123", 30), createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30), }, - mockBluecatCNAMEs: &[]BluecatCNAMERecord{ + mockBluecatCNAMEs: &[]api.BluecatCNAMERecord{ createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30), }, } @@ -252,12 +253,12 @@ func TestBluecatRecords(t *testing.T) { func TestBluecatApplyChangesCreate(t *testing.T) { client := mockGatewayClient{ - mockBluecatZones: &[]BluecatZone{ + mockBluecatZones: &[]api.BluecatZone{ createMockBluecatZone("example.com"), }, - mockBluecatHosts: &[]BluecatHostRecord{}, - mockBluecatCNAMEs: &[]BluecatCNAMERecord{}, - mockBluecatTXTs: &[]BluecatTXTRecord{}, + mockBluecatHosts: &[]api.BluecatHostRecord{}, + mockBluecatCNAMEs: &[]api.BluecatCNAMERecord{}, + mockBluecatTXTs: &[]api.BluecatTXTRecord{}, } provider := newBluecatProvider( @@ -279,18 +280,18 @@ func TestBluecatApplyChangesCreate(t *testing.T) { } func TestBluecatApplyChangesDelete(t *testing.T) { client := mockGatewayClient{ - mockBluecatZones: &[]BluecatZone{ + mockBluecatZones: &[]api.BluecatZone{ createMockBluecatZone("example.com"), }, - mockBluecatHosts: &[]BluecatHostRecord{ + mockBluecatHosts: &[]api.BluecatHostRecord{ createMockBluecatHostRecord("example.com", "123.123.123.122", 30), createMockBluecatHostRecord("nginx.example.com", "123.123.123.123", 30), createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30), }, - mockBluecatCNAMEs: &[]BluecatCNAMERecord{ + mockBluecatCNAMEs: &[]api.BluecatCNAMERecord{ createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30), }, - mockBluecatTXTs: &[]BluecatTXTRecord{ + mockBluecatTXTs: &[]api.BluecatTXTRecord{ createMockBluecatTXT("kdb.example.com", "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"), createMockBluecatTXT("wack.example.com", "hello"), createMockBluecatTXT("sack.example.com", ""), @@ -317,18 +318,18 @@ func TestBluecatApplyChangesDelete(t *testing.T) { func TestBluecatApplyChangesDeleteWithOwner(t *testing.T) { client := mockGatewayClient{ - mockBluecatZones: &[]BluecatZone{ + mockBluecatZones: &[]api.BluecatZone{ createMockBluecatZone("example.com"), }, - mockBluecatHosts: &[]BluecatHostRecord{ + mockBluecatHosts: &[]api.BluecatHostRecord{ createMockBluecatHostRecord("example.com", "123.123.123.122", 30), createMockBluecatHostRecord("nginx.example.com", "123.123.123.123", 30), createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30), }, - mockBluecatCNAMEs: &[]BluecatCNAMERecord{ + mockBluecatCNAMEs: &[]api.BluecatCNAMERecord{ createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30), }, - mockBluecatTXTs: &[]BluecatTXTRecord{ + mockBluecatTXTs: &[]api.BluecatTXTRecord{ createMockBluecatTXT("kdb.example.com", "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"), createMockBluecatTXT("wack.example.com", "hello"), createMockBluecatTXT("sack.example.com", ""), @@ -362,16 +363,6 @@ func TestBluecatApplyChangesDeleteWithOwner(t *testing.T) { } -func TestExpandZones(t *testing.T) { - mockZones := []string{"example.com", "nginx.example.com", "hack.example.com"} - expected := []string{"zones/com/zones/example/zones/", "zones/com/zones/example/zones/nginx/zones/", "zones/com/zones/example/zones/hack/zones/"} - for i := range mockZones { - if expandZone(mockZones[i]) != expected[i] { - t.Fatalf("%s", expected[i]) - } - } -} - func TestBluecatNewGatewayClient(t *testing.T) { testCookie := http.Cookie{Name: "testCookie", Value: "exampleCookie"} testToken := "exampleToken" @@ -382,7 +373,7 @@ func TestBluecatNewGatewayClient(t *testing.T) { testZone := "example.com" testVerify := true - client := NewGatewayClient(testCookie, testToken, testgateWayHost, testDNSConfiguration, testView, testZone, testDNSServer, testVerify) + client := api.NewGatewayClientConfig(testCookie, testToken, testgateWayHost, testDNSConfiguration, testView, testZone, testDNSServer, testVerify) if client.Cookie.Value != testCookie.Value || client.Cookie.Name != testCookie.Name || client.Token != testToken || client.Host != testgateWayHost || client.DNSConfiguration != testDNSConfiguration || client.View != testView || client.RootZone != testZone || client.SkipTLSVerify != testVerify { t.Fatal("Client values dont match") @@ -398,18 +389,18 @@ func TestBluecatNewGatewayClient(t *testing.T) { // TODO: Figure out why recordSet.res is not being set properly func TestBluecatRecordset(t *testing.T) { client := mockGatewayClient{ - mockBluecatZones: &[]BluecatZone{ + mockBluecatZones: &[]api.BluecatZone{ createMockBluecatZone("example.com"), }, - mockBluecatHosts: &[]BluecatHostRecord{ + mockBluecatHosts: &[]api.BluecatHostRecord{ createMockBluecatHostRecord("example.com", "123.123.123.122", 30), createMockBluecatHostRecord("nginx.example.com", "123.123.123.123", 30), createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30), }, - mockBluecatCNAMEs: &[]BluecatCNAMERecord{ + mockBluecatCNAMEs: &[]api.BluecatCNAMERecord{ createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30), }, - mockBluecatTXTs: &[]BluecatTXTRecord{ + mockBluecatTXTs: &[]api.BluecatTXTRecord{ createMockBluecatTXT("abc.example.com", "hello"), }, } @@ -420,11 +411,11 @@ func TestBluecatRecordset(t *testing.T) { // Test txt records for recordSet function testTxtEndpoint := endpoint.NewEndpoint("abc.example.com", endpoint.RecordTypeTXT, "hello") - txtObj := bluecatCreateTXTRecordRequest{ + txtObj := api.BluecatCreateTXTRecordRequest{ AbsoluteName: testTxtEndpoint.DNSName, Text: testTxtEndpoint.Targets[0], } - txtRecords := []BluecatTXTRecord{ + txtRecords := []api.BluecatTXTRecord{ createMockBluecatTXT("abc.example.com", "hello"), } expected := bluecatRecordSet{ @@ -440,11 +431,11 @@ func TestBluecatRecordset(t *testing.T) { // Test a records for recordSet function testHostEndpoint := endpoint.NewEndpoint("whitespace.example.com", endpoint.RecordTypeA, "123.123.123.124") - hostObj := bluecatCreateHostRecordRequest{ + hostObj := api.BluecatCreateHostRecordRequest{ AbsoluteName: testHostEndpoint.DNSName, IP4Address: testHostEndpoint.Targets[0], } - hostRecords := []BluecatHostRecord{ + hostRecords := []api.BluecatHostRecord{ createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30), } hostExpected := bluecatRecordSet{ @@ -460,11 +451,11 @@ func TestBluecatRecordset(t *testing.T) { // Test CName records for recordSet function testCnameEndpoint := endpoint.NewEndpoint("hack.example.com", endpoint.RecordTypeCNAME, "bluecatnetworks.com") - cnameObj := bluecatCreateCNAMERecordRequest{ + cnameObj := api.BluecatCreateCNAMERecordRequest{ AbsoluteName: testCnameEndpoint.DNSName, LinkedRecord: testCnameEndpoint.Targets[0], } - cnameRecords := []BluecatCNAMERecord{ + cnameRecords := []api.BluecatCNAMERecord{ createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30), } cnameExpected := bluecatRecordSet{ @@ -479,21 +470,6 @@ func TestBluecatRecordset(t *testing.T) { assert.Equal(t, cnameActual.res, cnameExpected.res) } -func TestValidDeployTypes(t *testing.T) { - validTypes := []string{"no-deploy", "full-deploy"} - invalidTypes := []string{"anything-else"} - for _, i := range validTypes { - if !isValidDNSDeployType(i) { - t.Fatalf("%s should be a valid deploy type", i) - } - } - for _, i := range invalidTypes { - if isValidDNSDeployType(i) { - t.Fatalf("%s should be a invalid deploy type", i) - } - } -} - func validateEndpoints(t *testing.T, actual, expected []*endpoint.Endpoint) { assert.True(t, testutils.SameEndpoints(actual, expected), "actual and expected endpoints don't match. %s:%s", actual, expected) } diff --git a/provider/bluecat/gateway/api.go b/provider/bluecat/gateway/api.go new file mode 100644 index 000000000..565989bda --- /dev/null +++ b/provider/bluecat/gateway/api.go @@ -0,0 +1,608 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "os" + "strings" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +// TODO: Ensure DNS Deploy Type Defaults to no-deploy instead of "" +type BluecatConfig struct { + GatewayHost string `json:"gatewayHost"` + GatewayUsername string `json:"gatewayUsername,omitempty"` + GatewayPassword string `json:"gatewayPassword,omitempty"` + DNSConfiguration string `json:"dnsConfiguration"` + DNSServerName string `json:"dnsServerName"` + DNSDeployType string `json:"dnsDeployType"` + View string `json:"dnsView"` + RootZone string `json:"rootZone"` + SkipTLSVerify bool `json:"skipTLSVerify"` +} + +type GatewayClient interface { + GetBluecatZones(zoneName string) ([]BluecatZone, error) + GetHostRecords(zone string, records *[]BluecatHostRecord) error + GetCNAMERecords(zone string, records *[]BluecatCNAMERecord) error + GetHostRecord(name string, record *BluecatHostRecord) error + GetCNAMERecord(name string, record *BluecatCNAMERecord) error + CreateHostRecord(zone string, req *BluecatCreateHostRecordRequest) (res interface{}, err error) + CreateCNAMERecord(zone string, req *BluecatCreateCNAMERecordRequest) (res interface{}, err error) + DeleteHostRecord(name string, zone string) (err error) + DeleteCNAMERecord(name string, zone string) (err error) + GetTXTRecords(zone string, records *[]BluecatTXTRecord) error + GetTXTRecord(name string, record *BluecatTXTRecord) error + CreateTXTRecord(zone string, req *BluecatCreateTXTRecordRequest) (res interface{}, err error) + DeleteTXTRecord(name string, zone string) error + ServerFullDeploy() error +} + +// GatewayClientConfig defines the configuration for a Bluecat Gateway Client +type GatewayClientConfig struct { + Cookie http.Cookie + Token string + Host string + DNSConfiguration string + View string + RootZone string + DNSServerName string + SkipTLSVerify bool +} + +// BluecatZone defines a zone to hold records +type BluecatZone struct { + ID int `json:"id"` + Name string `json:"name"` + Properties string `json:"properties"` + Type string `json:"type"` +} + +// BluecatHostRecord defines dns Host record +type BluecatHostRecord struct { + ID int `json:"id"` + Name string `json:"name"` + Properties string `json:"properties"` + Type string `json:"type"` +} + +// BluecatCNAMERecord defines dns CNAME record +type BluecatCNAMERecord struct { + ID int `json:"id"` + Name string `json:"name"` + Properties string `json:"properties"` + Type string `json:"type"` +} + +// BluecatTXTRecord defines dns TXT record +type BluecatTXTRecord struct { + ID int `json:"id"` + Name string `json:"name"` + Properties string `json:"properties"` +} + +type BluecatCreateHostRecordRequest struct { + AbsoluteName string `json:"absolute_name"` + IP4Address string `json:"ip4_address"` + TTL int `json:"ttl"` + Properties string `json:"properties"` +} + +type BluecatCreateCNAMERecordRequest struct { + AbsoluteName string `json:"absolute_name"` + LinkedRecord string `json:"linked_record"` + TTL int `json:"ttl"` + Properties string `json:"properties"` +} + +type BluecatCreateTXTRecordRequest struct { + AbsoluteName string `json:"absolute_name"` + Text string `json:"txt"` +} + +type BluecatServerFullDeployRequest struct { + ServerName string `json:"server_name"` +} + +// NewGatewayClient creates and returns a new Bluecat gateway client +func NewGatewayClientConfig(cookie http.Cookie, token, gatewayHost, dnsConfiguration, view, rootZone, dnsServerName string, skipTLSVerify bool) GatewayClientConfig { + // TODO: do not handle defaulting here + // + // Right now the Bluecat gateway doesn't seem to have a way to get the root zone from the API. If the user + // doesn't provide one via the config file we'll assume it's 'com' + if rootZone == "" { + rootZone = "com" + } + return GatewayClientConfig{ + Cookie: cookie, + Token: token, + Host: gatewayHost, + DNSConfiguration: dnsConfiguration, + DNSServerName: dnsServerName, + View: view, + RootZone: rootZone, + SkipTLSVerify: skipTLSVerify, + } +} + +// GetBluecatGatewayToken retrieves a Bluecat Gateway API token. +func GetBluecatGatewayToken(cfg BluecatConfig) (string, http.Cookie, error) { + var username string + if cfg.GatewayUsername != "" { + username = cfg.GatewayUsername + } + if v, ok := os.LookupEnv("BLUECAT_USERNAME"); ok { + username = v + } + + var password string + if cfg.GatewayPassword != "" { + password = cfg.GatewayPassword + } + if v, ok := os.LookupEnv("BLUECAT_PASSWORD"); ok { + password = v + } + + body, err := json.Marshal(map[string]string{ + "username": username, + "password": password, + }) + if err != nil { + return "", http.Cookie{}, errors.Wrap(err, "could not unmarshal credentials for bluecat gateway config") + } + + c := newHTTPClient(cfg.SkipTLSVerify) + + resp, err := c.Post(cfg.GatewayHost+"/rest_login", "application/json", bytes.NewBuffer(body)) + if err != nil { + return "", http.Cookie{}, errors.Wrap(err, "error obtaining API token from bluecat gateway") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + details, _ := ioutil.ReadAll(resp.Body) + return "", http.Cookie{}, errors.Errorf("got HTTP response code %v, detailed message: %v", resp.StatusCode, string(details)) + } + + res, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", http.Cookie{}, errors.Wrap(err, "error reading get_token response from bluecat gateway") + } + + resJSON := map[string]string{} + err = json.Unmarshal(res, &resJSON) + if err != nil { + return "", http.Cookie{}, errors.Wrap(err, "error unmarshaling json response (auth) from bluecat gateway") + } + + // Example response: {"access_token": "BAMAuthToken: abc123"} + // We only care about the actual token string - i.e. abc123 + // The gateway also creates a cookie as part of the response. This seems to be the actual auth mechanism, at least + // for now. + return strings.Split(resJSON["access_token"], " ")[1], *resp.Cookies()[0], nil +} + +func (c GatewayClientConfig) GetBluecatZones(zoneName string) ([]BluecatZone, error) { + client := newHTTPClient(c.SkipTLSVerify) + + zonePath := expandZone(zoneName) + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + req, err := c.buildHTTPRequest("GET", url, nil) + if err != nil { + return nil, errors.Wrap(err, "error building http request") + } + + resp, err := client.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "error retrieving zone(s) from gateway: %v, %v", url, zoneName) + } + + defer resp.Body.Close() + + zones := []BluecatZone{} + json.NewDecoder(resp.Body).Decode(&zones) + + // Bluecat Gateway only returns subzones one level deeper than the provided zone + // so this recursion is needed to traverse subzones until none are returned + for _, zone := range zones { + zoneProps := SplitProperties(zone.Properties) + subZones, err := c.GetBluecatZones(zoneProps["absoluteName"]) + if err != nil { + return nil, errors.Wrapf(err, "error retrieving subzones from gateway: %v", zoneName) + } + zones = append(zones, subZones...) + } + + return zones, nil +} + +func (c GatewayClientConfig) GetHostRecords(zone string, records *[]BluecatHostRecord) error { + client := newHTTPClient(c.SkipTLSVerify) + + zonePath := expandZone(zone) + + // Remove the trailing 'zones/' + zonePath = strings.TrimSuffix(zonePath, "zones/") + + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/" + req, err := c.buildHTTPRequest("GET", url, nil) + if err != nil { + return errors.Wrap(err, "error building http request") + } + + resp, err := client.Do(req) + if err != nil { + return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone) + } + + defer resp.Body.Close() + + json.NewDecoder(resp.Body).Decode(records) + log.Debugf("Get Host Records Response: %v", records) + + return nil +} + +func (c GatewayClientConfig) GetCNAMERecords(zone string, records *[]BluecatCNAMERecord) error { + client := newHTTPClient(c.SkipTLSVerify) + + zonePath := expandZone(zone) + + // Remove the trailing 'zones/' + zonePath = strings.TrimSuffix(zonePath, "zones/") + + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/" + req, err := c.buildHTTPRequest("GET", url, nil) + if err != nil { + return errors.Wrap(err, "error building http request") + } + + resp, err := client.Do(req) + if err != nil { + return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone) + } + + defer resp.Body.Close() + + json.NewDecoder(resp.Body).Decode(records) + log.Debugf("Get CName Records Response: %v", records) + + return nil +} + +func (c GatewayClientConfig) GetTXTRecords(zone string, records *[]BluecatTXTRecord) error { + client := newHTTPClient(c.SkipTLSVerify) + + zonePath := expandZone(zone) + + // Remove the trailing 'zones/' + zonePath = strings.TrimSuffix(zonePath, "zones/") + + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/" + req, err := c.buildHTTPRequest("GET", url, nil) + if err != nil { + return errors.Wrap(err, "error building http request") + } + log.Debugf("Request: %v", req) + + resp, err := client.Do(req) + if err != nil { + return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone) + } + log.Debugf("Get Txt Records response: %v", resp) + + defer resp.Body.Close() + json.NewDecoder(resp.Body).Decode(records) + log.Debugf("Get TXT Records Body: %v", records) + + return nil +} + +func (c GatewayClientConfig) GetHostRecord(name string, record *BluecatHostRecord) error { + client := newHTTPClient(c.SkipTLSVerify) + + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + + "/views/" + c.View + "/" + + "host_records/" + name + "/" + req, err := c.buildHTTPRequest("GET", url, nil) + if err != nil { + return errors.Wrapf(err, "error building http request: %v", name) + } + + resp, err := client.Do(req) + if err != nil { + return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name) + } + + defer resp.Body.Close() + + json.NewDecoder(resp.Body).Decode(record) + log.Debugf("Get Host Record Response: %v", record) + return nil +} + +func (c GatewayClientConfig) GetCNAMERecord(name string, record *BluecatCNAMERecord) error { + client := newHTTPClient(c.SkipTLSVerify) + + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + + "/views/" + c.View + "/" + + "cname_records/" + name + "/" + req, err := c.buildHTTPRequest("GET", url, nil) + if err != nil { + return errors.Wrapf(err, "error building http request: %v", name) + } + + resp, err := client.Do(req) + if err != nil { + return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name) + } + + defer resp.Body.Close() + + json.NewDecoder(resp.Body).Decode(record) + log.Debugf("Get CName Record Response: %v", record) + return nil +} + +func (c GatewayClientConfig) GetTXTRecord(name string, record *BluecatTXTRecord) error { + client := newHTTPClient(c.SkipTLSVerify) + + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + + "/views/" + c.View + "/" + + "text_records/" + name + "/" + + req, err := c.buildHTTPRequest("GET", url, nil) + if err != nil { + return errors.Wrap(err, "error building http request") + } + + resp, err := client.Do(req) + if err != nil { + return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name) + } + + defer resp.Body.Close() + json.NewDecoder(resp.Body).Decode(record) + log.Debugf("Get TXT Record Response: %v", record) + + return nil +} + +func (c GatewayClientConfig) CreateHostRecord(zone string, req *BluecatCreateHostRecordRequest) (res interface{}, err error) { + client := newHTTPClient(c.SkipTLSVerify) + + zonePath := expandZone(zone) + // Remove the trailing 'zones/' + zonePath = strings.TrimSuffix(zonePath, "zones/") + + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/" + body, _ := json.Marshal(req) + hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) + if err != nil { + return nil, errors.Wrap(err, "error building http request") + } + hreq.Header.Add("Content-Type", "application/json") + res, err = client.Do(hreq) + + return +} + +func (c GatewayClientConfig) CreateCNAMERecord(zone string, req *BluecatCreateCNAMERecordRequest) (res interface{}, err error) { + client := newHTTPClient(c.SkipTLSVerify) + + zonePath := expandZone(zone) + // Remove the trailing 'zones/' + zonePath = strings.TrimSuffix(zonePath, "zones/") + + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/" + body, _ := json.Marshal(req) + + hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) + if err != nil { + return nil, errors.Wrap(err, "error building http request") + } + + hreq.Header.Add("Content-Type", "application/json") + res, err = client.Do(hreq) + + return +} + +func (c GatewayClientConfig) CreateTXTRecord(zone string, req *BluecatCreateTXTRecordRequest) (interface{}, error) { + client := newHTTPClient(c.SkipTLSVerify) + + zonePath := expandZone(zone) + // Remove the trailing 'zones/' + zonePath = strings.TrimSuffix(zonePath, "zones/") + + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/" + body, _ := json.Marshal(req) + hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) + if err != nil { + return nil, err + } + + hreq.Header.Add("Content-Type", "application/json") + res, err := client.Do(hreq) + + return res, err +} + +func (c GatewayClientConfig) DeleteHostRecord(name string, zone string) (err error) { + client := newHTTPClient(c.SkipTLSVerify) + + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + + "/views/" + c.View + "/" + + "host_records/" + name + "." + zone + "/" + req, err := c.buildHTTPRequest("DELETE", url, nil) + if err != nil { + return errors.Wrapf(err, "error building http request: %v", name) + } + + _, err = client.Do(req) + if err != nil { + return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name) + } + + return nil +} + +func (c GatewayClientConfig) DeleteCNAMERecord(name string, zone string) (err error) { + client := newHTTPClient(c.SkipTLSVerify) + + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + + "/views/" + c.View + "/" + + "cname_records/" + name + "." + zone + "/" + req, err := c.buildHTTPRequest("DELETE", url, nil) + if err != nil { + return errors.Wrapf(err, "error building http request: %v", name) + } + + _, err = client.Do(req) + if err != nil { + return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name) + } + + return nil +} + +func (c GatewayClientConfig) DeleteTXTRecord(name string, zone string) error { + client := newHTTPClient(c.SkipTLSVerify) + + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + + "/views/" + c.View + "/" + + "text_records/" + name + "." + zone + "/" + + req, err := c.buildHTTPRequest("DELETE", url, nil) + if err != nil { + return errors.Wrap(err, "error building http request") + } + + _, err = client.Do(req) + if err != nil { + return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name) + } + + return nil +} + +func (c GatewayClientConfig) ServerFullDeploy() error { + log.Infof("Executing full deploy on server %s", c.DNSServerName) + httpClient := newHTTPClient(c.SkipTLSVerify) + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/server/full_deploy/" + requestBody := BluecatServerFullDeployRequest{ + ServerName: c.DNSServerName, + } + + body, err := json.Marshal(requestBody) + if err != nil { + return errors.Wrap(err, "could not marshal body for server full deploy") + } + + request, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) + if err != nil { + return errors.Wrap(err, "error building http request") + } + + request.Header.Add("Content-Type", "application/json") + response, err := httpClient.Do(request) + if err != nil { + return errors.Wrap(err, "error executing full deploy") + } + + if response.StatusCode != http.StatusCreated { + responseBody, err := ioutil.ReadAll(response.Body) + if err != nil { + return errors.Wrap(err, "failed to read full deploy response body") + } + return errors.Errorf("got HTTP response code %v, detailed message: %v", response.StatusCode, string(responseBody)) + } + + return nil +} + +// buildHTTPRequest builds a standard http Request and adds authentication headers required by Bluecat Gateway +func (c GatewayClientConfig) buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error) { + req, err := http.NewRequest(method, url, body) + req.Header.Add("Accept", "application/json") + req.Header.Add("Authorization", "Basic "+c.Token) + req.AddCookie(&c.Cookie) + return req, err +} + +// SplitProperties is a helper function to break a '|' separated string into key/value pairs +// i.e. "foo=bar|baz=mop" +func SplitProperties(props string) map[string]string { + propMap := make(map[string]string) + // remove trailing | character before we split + props = strings.TrimSuffix(props, "|") + + splits := strings.Split(props, "|") + for _, pair := range splits { + items := strings.Split(pair, "=") + propMap[items[0]] = items[1] + } + + return propMap +} + +// IsValidDNSDeployType validates the deployment type provided by a users configuration is supported by the Bluecat Provider. +func IsValidDNSDeployType(deployType string) bool { + validDNSDeployTypes := []string{"no-deploy", "full-deploy"} + for _, t := range validDNSDeployTypes { + if t == deployType { + return true + } + } + return false +} + +// expandZone takes an absolute domain name such as 'example.com' and returns a zone hierarchy used by Bluecat Gateway, +// such as '/zones/com/zones/example/zones/' +func expandZone(zone string) string { + ze := "zones/" + parts := strings.Split(zone, ".") + if len(parts) > 1 { + last := len(parts) - 1 + for i := range parts { + ze = ze + parts[last-i] + "/zones/" + } + } else { + ze = ze + zone + "/zones/" + } + return ze +} + +// newHTTPClient returns an instance of http client +func newHTTPClient(skipTLSVerify bool) *http.Client { + return &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: skipTLSVerify, + }, + }, + } +} diff --git a/provider/bluecat/gateway/api_test.go b/provider/bluecat/gateway/api_test.go new file mode 100644 index 000000000..68142a9c6 --- /dev/null +++ b/provider/bluecat/gateway/api_test.go @@ -0,0 +1,43 @@ +/* +Copyright 2020 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + "testing" +) + +func TestExpandZones(t *testing.T) { + mockZones := []string{"example.com", "nginx.example.com", "hack.example.com"} + expected := []string{"zones/com/zones/example/zones/", "zones/com/zones/example/zones/nginx/zones/", "zones/com/zones/example/zones/hack/zones/"} + for i := range mockZones { + if expandZone(mockZones[i]) != expected[i] { + t.Fatalf("%s", expected[i]) + } + } +} + +func TestValidDeployTypes(t *testing.T) { + validTypes := []string{"no-deploy", "full-deploy"} + invalidTypes := []string{"anything-else"} + for _, i := range validTypes { + if !IsValidDNSDeployType(i) { + t.Fatalf("%s should be a valid deploy type", i) + } + } + for _, i := range invalidTypes { + if IsValidDNSDeployType(i) { + t.Fatalf("%s should be a invalid deploy type", i) + } + } +} From 17d4371f60b463e2edd7b36887b91edaf1a517c5 Mon Sep 17 00:00:00 2001 From: Vinny Sabatini Date: Fri, 11 Mar 2022 16:15:52 -0600 Subject: [PATCH 2/8] provider/bluecat: improve api error handling Check status codes when API calls are made for gets and deletes. All of the creates were done slightly different and will be updated in a separate commit. --- provider/bluecat/gateway/api.go | 77 ++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/provider/bluecat/gateway/api.go b/provider/bluecat/gateway/api.go index 565989bda..dfbba374c 100644 --- a/provider/bluecat/gateway/api.go +++ b/provider/bluecat/gateway/api.go @@ -216,11 +216,14 @@ func (c GatewayClientConfig) GetBluecatZones(zoneName string) ([]BluecatZone, er resp, err := client.Do(req) if err != nil { - return nil, errors.Wrapf(err, "error retrieving zone(s) from gateway: %v, %v", url, zoneName) + return nil, errors.Wrapf(err, "error requesting zones from gateway: %v, %v", url, zoneName) } - defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.Errorf("received http %v requesting zones from gateway in zone %v", resp.StatusCode, zoneName) + } + zones := []BluecatZone{} json.NewDecoder(resp.Body).Decode(&zones) @@ -254,11 +257,14 @@ func (c GatewayClientConfig) GetHostRecords(zone string, records *[]BluecatHostR resp, err := client.Do(req) if err != nil { - return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone) + return errors.Wrapf(err, "error requesting host records from gateway in zone %v", zone) } - defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return errors.Errorf("received http %v requesting host records from gateway in zone %v", resp.StatusCode, zone) + } + json.NewDecoder(resp.Body).Decode(records) log.Debugf("Get Host Records Response: %v", records) @@ -281,11 +287,14 @@ func (c GatewayClientConfig) GetCNAMERecords(zone string, records *[]BluecatCNAM resp, err := client.Do(req) if err != nil { - return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone) + return errors.Wrapf(err, "error retrieving cname records from gateway in zone %v", zone) } - defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return errors.Errorf("received http %v requesting cname records from gateway in zone %v", resp.StatusCode, zone) + } + json.NewDecoder(resp.Body).Decode(records) log.Debugf("Get CName Records Response: %v", records) @@ -309,11 +318,15 @@ func (c GatewayClientConfig) GetTXTRecords(zone string, records *[]BluecatTXTRec resp, err := client.Do(req) if err != nil { - return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", zone) + return errors.Wrapf(err, "error retrieving txt records from gateway in zone %v", zone) } - log.Debugf("Get Txt Records response: %v", resp) - defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return errors.Errorf("received http %v requesting txt records from gateway in zone %v", resp.StatusCode, zone) + } + + log.Debugf("Get Txt Records response: %v", resp) json.NewDecoder(resp.Body).Decode(records) log.Debugf("Get TXT Records Body: %v", records) @@ -333,11 +346,14 @@ func (c GatewayClientConfig) GetHostRecord(name string, record *BluecatHostRecor resp, err := client.Do(req) if err != nil { - return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name) + return errors.Wrapf(err, "error retrieving host record %v from gateway", name) } - defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return errors.Errorf("received http %v while retrieving host record %v from gateway", resp.StatusCode, name) + } + json.NewDecoder(resp.Body).Decode(record) log.Debugf("Get Host Record Response: %v", record) return nil @@ -356,11 +372,14 @@ func (c GatewayClientConfig) GetCNAMERecord(name string, record *BluecatCNAMERec resp, err := client.Do(req) if err != nil { - return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name) + return errors.Wrapf(err, "error retrieving cname record %v from gateway", name) } - defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return errors.Errorf("received http %v while retrieving cname record %v from gateway", resp.StatusCode, name) + } + json.NewDecoder(resp.Body).Decode(record) log.Debugf("Get CName Record Response: %v", record) return nil @@ -380,10 +399,14 @@ func (c GatewayClientConfig) GetTXTRecord(name string, record *BluecatTXTRecord) resp, err := client.Do(req) if err != nil { - return errors.Wrapf(err, "error retrieving record(s) from gateway: %v", name) + return errors.Wrapf(err, "error retrieving record %v from gateway", name) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return errors.Errorf("received http %v while retrieving txt record %v from gateway", resp.StatusCode, name) } - defer resp.Body.Close() json.NewDecoder(resp.Body).Decode(record) log.Debugf("Get TXT Record Response: %v", record) @@ -461,9 +484,13 @@ func (c GatewayClientConfig) DeleteHostRecord(name string, zone string) (err err return errors.Wrapf(err, "error building http request: %v", name) } - _, err = client.Do(req) + resp, err := client.Do(req) if err != nil { - return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name) + return errors.Wrapf(err, "error deleting host record %v from gateway", name) + } + + if resp.StatusCode != http.StatusOK { + return errors.Errorf("received http %v while deleting host record %v from gateway", resp.StatusCode, name) } return nil @@ -480,9 +507,13 @@ func (c GatewayClientConfig) DeleteCNAMERecord(name string, zone string) (err er return errors.Wrapf(err, "error building http request: %v", name) } - _, err = client.Do(req) + resp, err := client.Do(req) if err != nil { - return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name) + return errors.Wrapf(err, "error deleting cname record %v from gateway", name) + } + + if resp.StatusCode != http.StatusOK { + return errors.Errorf("received http %v while deleting cname record %v from gateway", resp.StatusCode, name) } return nil @@ -500,9 +531,13 @@ func (c GatewayClientConfig) DeleteTXTRecord(name string, zone string) error { return errors.Wrap(err, "error building http request") } - _, err = client.Do(req) + resp, err := client.Do(req) if err != nil { - return errors.Wrapf(err, "error deleting record(s) from gateway: %v", name) + return errors.Wrapf(err, "error deleting txt record %v from gateway", name) + } + + if resp.StatusCode != http.StatusOK { + return errors.Errorf("received http %v while deleting txt record %v from gateway", resp.StatusCode, name) } return nil From 5296a15fdf1c80b249d505f4386d1ba3b9fc8ded Mon Sep 17 00:00:00 2001 From: Vinny Sabatini Date: Fri, 11 Mar 2022 17:28:03 -0600 Subject: [PATCH 3/8] provider/bluecat: handle http errors for create in API --- provider/bluecat/bluecat.go | 6 ++-- provider/bluecat/bluecat_test.go | 12 +++---- provider/bluecat/gateway/api.go | 54 +++++++++++++++++++++++--------- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/provider/bluecat/bluecat.go b/provider/bluecat/bluecat.go index 06ef10e5f..db1e7aff4 100644 --- a/provider/bluecat/bluecat.go +++ b/provider/bluecat/bluecat.go @@ -345,11 +345,11 @@ func (p *BluecatProvider) createRecords(created bluecatChangeMap) { var response interface{} switch ep.RecordType { case endpoint.RecordTypeA: - response, err = p.gatewayClient.CreateHostRecord(zone, recordSet.obj.(*api.BluecatCreateHostRecordRequest)) + err = p.gatewayClient.CreateHostRecord(zone, recordSet.obj.(*api.BluecatCreateHostRecordRequest)) case endpoint.RecordTypeCNAME: - response, err = p.gatewayClient.CreateCNAMERecord(zone, recordSet.obj.(*api.BluecatCreateCNAMERecordRequest)) + err = p.gatewayClient.CreateCNAMERecord(zone, recordSet.obj.(*api.BluecatCreateCNAMERecordRequest)) case endpoint.RecordTypeTXT: - response, err = p.gatewayClient.CreateTXTRecord(zone, recordSet.obj.(*api.BluecatCreateTXTRecordRequest)) + err = p.gatewayClient.CreateTXTRecord(zone, recordSet.obj.(*api.BluecatCreateTXTRecordRequest)) } log.Debugf("Response from create: %v", response) if err != nil { diff --git a/provider/bluecat/bluecat_test.go b/provider/bluecat/bluecat_test.go index 13ee8f762..bf2d3b17b 100644 --- a/provider/bluecat/bluecat_test.go +++ b/provider/bluecat/bluecat_test.go @@ -76,11 +76,11 @@ func (g mockGatewayClient) GetCNAMERecord(name string, record *api.BluecatCNAMER } return nil } -func (g mockGatewayClient) CreateHostRecord(zone string, req *api.BluecatCreateHostRecordRequest) (res interface{}, err error) { - return nil, nil +func (g mockGatewayClient) CreateHostRecord(zone string, req *api.BluecatCreateHostRecordRequest) (err error) { + return nil } -func (g mockGatewayClient) CreateCNAMERecord(zone string, req *api.BluecatCreateCNAMERecordRequest) (res interface{}, err error) { - return nil, nil +func (g mockGatewayClient) CreateCNAMERecord(zone string, req *api.BluecatCreateCNAMERecordRequest) (err error) { + return nil } func (g mockGatewayClient) DeleteHostRecord(name string, zone string) (err error) { *g.mockBluecatHosts = nil @@ -103,8 +103,8 @@ func (g mockGatewayClient) GetTXTRecord(name string, record *api.BluecatTXTRecor } return nil } -func (g mockGatewayClient) CreateTXTRecord(zone string, req *api.BluecatCreateTXTRecordRequest) (res interface{}, err error) { - return nil, nil +func (g mockGatewayClient) CreateTXTRecord(zone string, req *api.BluecatCreateTXTRecordRequest) error { + return nil } func (g mockGatewayClient) DeleteTXTRecord(name string, zone string) error { *g.mockBluecatTXTs = nil diff --git a/provider/bluecat/gateway/api.go b/provider/bluecat/gateway/api.go index dfbba374c..8f022ba4b 100644 --- a/provider/bluecat/gateway/api.go +++ b/provider/bluecat/gateway/api.go @@ -49,13 +49,13 @@ type GatewayClient interface { GetCNAMERecords(zone string, records *[]BluecatCNAMERecord) error GetHostRecord(name string, record *BluecatHostRecord) error GetCNAMERecord(name string, record *BluecatCNAMERecord) error - CreateHostRecord(zone string, req *BluecatCreateHostRecordRequest) (res interface{}, err error) - CreateCNAMERecord(zone string, req *BluecatCreateCNAMERecordRequest) (res interface{}, err error) + CreateHostRecord(zone string, req *BluecatCreateHostRecordRequest) error + CreateCNAMERecord(zone string, req *BluecatCreateCNAMERecordRequest) error DeleteHostRecord(name string, zone string) (err error) DeleteCNAMERecord(name string, zone string) (err error) GetTXTRecords(zone string, records *[]BluecatTXTRecord) error GetTXTRecord(name string, record *BluecatTXTRecord) error - CreateTXTRecord(zone string, req *BluecatCreateTXTRecordRequest) (res interface{}, err error) + CreateTXTRecord(zone string, req *BluecatCreateTXTRecordRequest) error DeleteTXTRecord(name string, zone string) error ServerFullDeploy() error } @@ -413,7 +413,7 @@ func (c GatewayClientConfig) GetTXTRecord(name string, record *BluecatTXTRecord) return nil } -func (c GatewayClientConfig) CreateHostRecord(zone string, req *BluecatCreateHostRecordRequest) (res interface{}, err error) { +func (c GatewayClientConfig) CreateHostRecord(zone string, req *BluecatCreateHostRecordRequest) error { client := newHTTPClient(c.SkipTLSVerify) zonePath := expandZone(zone) @@ -424,15 +424,23 @@ func (c GatewayClientConfig) CreateHostRecord(zone string, req *BluecatCreateHos body, _ := json.Marshal(req) hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) if err != nil { - return nil, errors.Wrap(err, "error building http request") + return errors.Wrap(err, "error building http request") } hreq.Header.Add("Content-Type", "application/json") - res, err = client.Do(hreq) + resp, err := client.Do(hreq) + if err != nil { + return errors.Wrapf(err, "error creating host record %v in gateway", req.AbsoluteName) + } + defer resp.Body.Close() - return + if resp.StatusCode != http.StatusCreated { + return errors.Errorf("received http %v while creating host record %v in gateway", resp.StatusCode, req.AbsoluteName) + } + + return nil } -func (c GatewayClientConfig) CreateCNAMERecord(zone string, req *BluecatCreateCNAMERecordRequest) (res interface{}, err error) { +func (c GatewayClientConfig) CreateCNAMERecord(zone string, req *BluecatCreateCNAMERecordRequest) error { client := newHTTPClient(c.SkipTLSVerify) zonePath := expandZone(zone) @@ -444,16 +452,24 @@ func (c GatewayClientConfig) CreateCNAMERecord(zone string, req *BluecatCreateCN hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) if err != nil { - return nil, errors.Wrap(err, "error building http request") + return errors.Wrap(err, "error building http request") } hreq.Header.Add("Content-Type", "application/json") - res, err = client.Do(hreq) + resp, err := client.Do(hreq) + if err != nil { + return errors.Wrapf(err, "error creating cname record %v in gateway", req.AbsoluteName) + } + defer resp.Body.Close() - return + if resp.StatusCode != http.StatusCreated { + return errors.Errorf("received http %v while creating cname record %v to alias %v in gateway", resp.StatusCode, req.AbsoluteName, req.LinkedRecord) + } + + return nil } -func (c GatewayClientConfig) CreateTXTRecord(zone string, req *BluecatCreateTXTRecordRequest) (interface{}, error) { +func (c GatewayClientConfig) CreateTXTRecord(zone string, req *BluecatCreateTXTRecordRequest) error { client := newHTTPClient(c.SkipTLSVerify) zonePath := expandZone(zone) @@ -464,13 +480,21 @@ func (c GatewayClientConfig) CreateTXTRecord(zone string, req *BluecatCreateTXTR body, _ := json.Marshal(req) hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) if err != nil { - return nil, err + return errors.Wrap(err, "error building http request") } hreq.Header.Add("Content-Type", "application/json") - res, err := client.Do(hreq) + resp, err := client.Do(hreq) + if err != nil { + return errors.Wrapf(err, "error creating txt record %v in gateway", req.AbsoluteName) + } + defer resp.Body.Close() - return res, err + if resp.StatusCode != http.StatusCreated { + return errors.Errorf("received http %v while creating txt record %v in gateway", resp.StatusCode, req.AbsoluteName) + } + + return nil } func (c GatewayClientConfig) DeleteHostRecord(name string, zone string) (err error) { From cbe19695112526d7584d1ae9a0b3823171560759 Mon Sep 17 00:00:00 2001 From: Vinny Sabatini Date: Fri, 18 Mar 2022 15:30:49 -0500 Subject: [PATCH 4/8] provider/bluecat: fix txt pre/suffix bug The prefix and suffix for the txt record was not being taken into account when trying to find an owner for a cname and host record. In addition, CName records have to be unique values, therefore txt records need to have either a prefix or suffix included. This fixes the "owner" logic to ensure when checking a corresponding txt recod against a cname record that the prefix or suffix is used when comparing records. Also, correct the response codes for the DELETE calls to Bluecat Gateway --- main.go | 2 +- provider/bluecat/bluecat.go | 10 +++++++--- provider/bluecat/bluecat_test.go | 5 +++-- provider/bluecat/gateway/api.go | 8 ++++---- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/main.go b/main.go index a4f5ed00b..c156edd8b 100644 --- a/main.go +++ b/main.go @@ -211,7 +211,7 @@ func main() { case "azure-private-dns": p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun) case "bluecat": - p, err = bluecat.NewBluecatProvider(cfg.BluecatConfigFile, cfg.BluecatDNSConfiguration, cfg.BluecatDNSServerName, cfg.BluecatDNSDeployType, cfg.BluecatDNSView, cfg.BluecatGatewayHost, cfg.BluecatRootZone, domainFilter, zoneIDFilter, cfg.DryRun, cfg.BluecatSkipTLSVerify) + p, err = bluecat.NewBluecatProvider(cfg.BluecatConfigFile, cfg.BluecatDNSConfiguration, cfg.BluecatDNSServerName, cfg.BluecatDNSDeployType, cfg.BluecatDNSView, cfg.BluecatGatewayHost, cfg.BluecatRootZone, cfg.TXTPrefix, cfg.TXTSuffix, domainFilter, zoneIDFilter, cfg.DryRun, cfg.BluecatSkipTLSVerify) case "vinyldns": p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun) case "vultr": diff --git a/provider/bluecat/bluecat.go b/provider/bluecat/bluecat.go index db1e7aff4..70bdbabea 100644 --- a/provider/bluecat/bluecat.go +++ b/provider/bluecat/bluecat.go @@ -51,6 +51,8 @@ type BluecatProvider struct { DNSDeployType string View string gatewayClient api.GatewayClient + TxtPrefix string + TxtSuffix string } type bluecatRecordSet struct { @@ -61,7 +63,7 @@ type bluecatRecordSet struct { // NewBluecatProvider creates a new Bluecat provider. // // Returns a pointer to the provider or an error if a provider could not be created. -func NewBluecatProvider(configFile, dnsConfiguration, dnsServerName, dnsDeployType, dnsView, gatewayHost, rootZone string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun, skipTLSVerify bool) (*BluecatProvider, error) { +func NewBluecatProvider(configFile, dnsConfiguration, dnsServerName, dnsDeployType, dnsView, gatewayHost, rootZone, txtPrefix, txtSuffix string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun, skipTLSVerify bool) (*BluecatProvider, error) { cfg := api.BluecatConfig{} contents, err := os.ReadFile(configFile) if err != nil { @@ -107,6 +109,8 @@ func NewBluecatProvider(configFile, dnsConfiguration, dnsServerName, dnsDeployTy DNSDeployType: cfg.DNSDeployType, View: cfg.View, RootZone: cfg.RootZone, + TxtPrefix: txtPrefix, + TxtSuffix: txtSuffix, } return provider, nil } @@ -156,7 +160,7 @@ func (p *BluecatProvider) Records(ctx context.Context) (endpoints []*endpoint.En ep = endpoint.NewEndpoint(propMap["absoluteName"], endpoint.RecordTypeA, ip) } for _, txtRec := range resT { - if strings.Compare(rec.Name, txtRec.Name) == 0 { + if strings.Compare(p.TxtPrefix+rec.Name+p.TxtSuffix, txtRec.Name) == 0 { ep.Labels[endpoint.OwnerLabelKey], err = extractOwnerfromTXTRecord(txtRec.Properties) if err != nil { log.Debugf("External DNS Owner %s", err) @@ -185,7 +189,7 @@ func (p *BluecatProvider) Records(ctx context.Context) (endpoints []*endpoint.En ep = endpoint.NewEndpoint(propMap["absoluteName"], endpoint.RecordTypeCNAME, propMap["linkedRecordName"]) } for _, txtRec := range resT { - if strings.Compare(rec.Name, txtRec.Name) == 0 { + if strings.Compare(p.TxtPrefix+rec.Name+p.TxtSuffix, txtRec.Name) == 0 { ep.Labels[endpoint.OwnerLabelKey], err = extractOwnerfromTXTRecord(txtRec.Properties) if err != nil { log.Debugf("External DNS Owner %s", err) diff --git a/provider/bluecat/bluecat_test.go b/provider/bluecat/bluecat_test.go index bf2d3b17b..a877145e5 100644 --- a/provider/bluecat/bluecat_test.go +++ b/provider/bluecat/bluecat_test.go @@ -345,9 +345,10 @@ func TestBluecatApplyChangesDeleteWithOwner(t *testing.T) { if strings.Contains(ep.Targets.String(), "external-dns") { owner, err := extractOwnerfromTXTRecord(ep.Targets.String()) if err != nil { - continue + t.Logf("%v", err) + } else { + t.Logf("Owner %s", owner) } - t.Logf("Owner %s %s", owner, err) } } err := provider.ApplyChanges(context.Background(), &plan.Changes{Delete: ti.Endpoints}) diff --git a/provider/bluecat/gateway/api.go b/provider/bluecat/gateway/api.go index 8f022ba4b..887f0dcef 100644 --- a/provider/bluecat/gateway/api.go +++ b/provider/bluecat/gateway/api.go @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ - +// TODO: add logging package api import ( @@ -513,7 +513,7 @@ func (c GatewayClientConfig) DeleteHostRecord(name string, zone string) (err err return errors.Wrapf(err, "error deleting host record %v from gateway", name) } - if resp.StatusCode != http.StatusOK { + if resp.StatusCode != http.StatusNoContent { return errors.Errorf("received http %v while deleting host record %v from gateway", resp.StatusCode, name) } @@ -536,7 +536,7 @@ func (c GatewayClientConfig) DeleteCNAMERecord(name string, zone string) (err er return errors.Wrapf(err, "error deleting cname record %v from gateway", name) } - if resp.StatusCode != http.StatusOK { + if resp.StatusCode != http.StatusNoContent { return errors.Errorf("received http %v while deleting cname record %v from gateway", resp.StatusCode, name) } @@ -560,7 +560,7 @@ func (c GatewayClientConfig) DeleteTXTRecord(name string, zone string) error { return errors.Wrapf(err, "error deleting txt record %v from gateway", name) } - if resp.StatusCode != http.StatusOK { + if resp.StatusCode != http.StatusNoContent { return errors.Errorf("received http %v while deleting txt record %v from gateway", resp.StatusCode, name) } From 5851d437ead03b9e7d3daf1af97292e23ecb9899 Mon Sep 17 00:00:00 2001 From: Vinny Sabatini Date: Fri, 25 Mar 2022 16:08:40 -0500 Subject: [PATCH 5/8] provider/bluecat: do not deploy dns on dry-run --- provider/bluecat/bluecat.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/provider/bluecat/bluecat.go b/provider/bluecat/bluecat.go index 70bdbabea..2c25406fb 100644 --- a/provider/bluecat/bluecat.go +++ b/provider/bluecat/bluecat.go @@ -221,14 +221,18 @@ func (p *BluecatProvider) ApplyChanges(ctx context.Context, changes *plan.Change p.createRecords(created) if p.DNSServerName != "" { - switch p.DNSDeployType { - case "full-deploy": - err := p.gatewayClient.ServerFullDeploy() - if err != nil { - return err + if p.dryRun { + log.Debug("Not executing deploy because this is running in dry-run mode") + } else { + switch p.DNSDeployType { + case "full-deploy": + err := p.gatewayClient.ServerFullDeploy() + if err != nil { + return err + } + case "no-deploy": + log.Debug("Not executing deploy because DNSDeployType is set to 'no-deploy'") } - case "no-deploy": - log.Debug("Not executing deploy because DNSDeployType is set to 'no-deploy'") } } else { log.Debug("Not executing deploy because server name was not provided") From 3b681907782952d02cdb03bf99f82235c22c1fd0 Mon Sep 17 00:00:00 2001 From: Vinny Sabatini Date: Fri, 25 Mar 2022 17:05:47 -0500 Subject: [PATCH 6/8] provider/bluecat: extract http requests into function Consolidating the HTTP requests into a function will help ensure consistency when making HTTP requests to the Bluecat gateway, and in a future commit will make mocking these requests easier --- provider/bluecat/gateway/api.go | 272 +++++++++++--------------------- 1 file changed, 95 insertions(+), 177 deletions(-) diff --git a/provider/bluecat/gateway/api.go b/provider/bluecat/gateway/api.go index 887f0dcef..2f4389540 100644 --- a/provider/bluecat/gateway/api.go +++ b/provider/bluecat/gateway/api.go @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ // TODO: add logging +// TODO: add timeouts package api import ( @@ -172,27 +173,26 @@ func GetBluecatGatewayToken(cfg BluecatConfig) (string, http.Cookie, error) { if err != nil { return "", http.Cookie{}, errors.Wrap(err, "could not unmarshal credentials for bluecat gateway config") } + url := cfg.GatewayHost + "/rest_login" - c := newHTTPClient(cfg.SkipTLSVerify) + response, err := executeHTTPRequest(cfg.SkipTLSVerify, http.MethodPost, url, "", bytes.NewBuffer(body), http.Cookie{}) - resp, err := c.Post(cfg.GatewayHost+"/rest_login", "application/json", bytes.NewBuffer(body)) if err != nil { return "", http.Cookie{}, errors.Wrap(err, "error obtaining API token from bluecat gateway") } - defer resp.Body.Close() + defer response.Body.Close() - if resp.StatusCode != http.StatusOK { - details, _ := ioutil.ReadAll(resp.Body) - return "", http.Cookie{}, errors.Errorf("got HTTP response code %v, detailed message: %v", resp.StatusCode, string(details)) - } - - res, err := ioutil.ReadAll(resp.Body) + responseBody, err := ioutil.ReadAll(response.Body) if err != nil { - return "", http.Cookie{}, errors.Wrap(err, "error reading get_token response from bluecat gateway") + return "", http.Cookie{}, errors.Wrap(err, "failed to read login response from bluecat gateway") } - resJSON := map[string]string{} - err = json.Unmarshal(res, &resJSON) + if response.StatusCode != http.StatusOK { + return "", http.Cookie{}, errors.Errorf("got HTTP response code %v, detailed message: %v", response.StatusCode, string(responseBody)) + } + + jsonResponse := map[string]string{} + err = json.Unmarshal(responseBody, &jsonResponse) if err != nil { return "", http.Cookie{}, errors.Wrap(err, "error unmarshaling json response (auth) from bluecat gateway") } @@ -201,31 +201,25 @@ func GetBluecatGatewayToken(cfg BluecatConfig) (string, http.Cookie, error) { // We only care about the actual token string - i.e. abc123 // The gateway also creates a cookie as part of the response. This seems to be the actual auth mechanism, at least // for now. - return strings.Split(resJSON["access_token"], " ")[1], *resp.Cookies()[0], nil + return strings.Split(jsonResponse["access_token"], " ")[1], *response.Cookies()[0], nil } func (c GatewayClientConfig) GetBluecatZones(zoneName string) ([]BluecatZone, error) { - client := newHTTPClient(c.SkipTLSVerify) - zonePath := expandZone(zoneName) url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return nil, errors.Wrap(err, "error building http request") - } - resp, err := client.Do(req) + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) if err != nil { return nil, errors.Wrapf(err, "error requesting zones from gateway: %v, %v", url, zoneName) } - defer resp.Body.Close() + defer response.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, errors.Errorf("received http %v requesting zones from gateway in zone %v", resp.StatusCode, zoneName) + if response.StatusCode != http.StatusOK { + return nil, errors.Errorf("received http %v requesting zones from gateway in zone %v", response.StatusCode, zoneName) } zones := []BluecatZone{} - json.NewDecoder(resp.Body).Decode(&zones) + json.NewDecoder(response.Body).Decode(&zones) // Bluecat Gateway only returns subzones one level deeper than the provided zone // so this recursion is needed to traverse subzones until none are returned @@ -242,326 +236,252 @@ func (c GatewayClientConfig) GetBluecatZones(zoneName string) ([]BluecatZone, er } func (c GatewayClientConfig) GetHostRecords(zone string, records *[]BluecatHostRecord) error { - client := newHTTPClient(c.SkipTLSVerify) - zonePath := expandZone(zone) - // Remove the trailing 'zones/' zonePath = strings.TrimSuffix(zonePath, "zones/") url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/" - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return errors.Wrap(err, "error building http request") - } - resp, err := client.Do(req) + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) if err != nil { return errors.Wrapf(err, "error requesting host records from gateway in zone %v", zone) } - defer resp.Body.Close() + defer response.Body.Close() - if resp.StatusCode != http.StatusOK { - return errors.Errorf("received http %v requesting host records from gateway in zone %v", resp.StatusCode, zone) + if response.StatusCode != http.StatusOK { + return errors.Errorf("received http %v requesting host records from gateway in zone %v", response.StatusCode, zone) } - json.NewDecoder(resp.Body).Decode(records) + json.NewDecoder(response.Body).Decode(records) log.Debugf("Get Host Records Response: %v", records) return nil } func (c GatewayClientConfig) GetCNAMERecords(zone string, records *[]BluecatCNAMERecord) error { - client := newHTTPClient(c.SkipTLSVerify) - zonePath := expandZone(zone) - // Remove the trailing 'zones/' zonePath = strings.TrimSuffix(zonePath, "zones/") url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/" - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return errors.Wrap(err, "error building http request") - } - resp, err := client.Do(req) + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) if err != nil { return errors.Wrapf(err, "error retrieving cname records from gateway in zone %v", zone) } - defer resp.Body.Close() + defer response.Body.Close() - if resp.StatusCode != http.StatusOK { - return errors.Errorf("received http %v requesting cname records from gateway in zone %v", resp.StatusCode, zone) + if response.StatusCode != http.StatusOK { + return errors.Errorf("received http %v requesting cname records from gateway in zone %v", response.StatusCode, zone) } - json.NewDecoder(resp.Body).Decode(records) + json.NewDecoder(response.Body).Decode(records) log.Debugf("Get CName Records Response: %v", records) return nil } func (c GatewayClientConfig) GetTXTRecords(zone string, records *[]BluecatTXTRecord) error { - client := newHTTPClient(c.SkipTLSVerify) - zonePath := expandZone(zone) - // Remove the trailing 'zones/' zonePath = strings.TrimSuffix(zonePath, "zones/") url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/" - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return errors.Wrap(err, "error building http request") - } - log.Debugf("Request: %v", req) - resp, err := client.Do(req) + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) if err != nil { return errors.Wrapf(err, "error retrieving txt records from gateway in zone %v", zone) } - defer resp.Body.Close() + defer response.Body.Close() - if resp.StatusCode != http.StatusOK { - return errors.Errorf("received http %v requesting txt records from gateway in zone %v", resp.StatusCode, zone) + if response.StatusCode != http.StatusOK { + return errors.Errorf("received http %v requesting txt records from gateway in zone %v", response.StatusCode, zone) } - log.Debugf("Get Txt Records response: %v", resp) - json.NewDecoder(resp.Body).Decode(records) + log.Debugf("Get Txt Records response: %v", response) + json.NewDecoder(response.Body).Decode(records) log.Debugf("Get TXT Records Body: %v", records) return nil } func (c GatewayClientConfig) GetHostRecord(name string, record *BluecatHostRecord) error { - client := newHTTPClient(c.SkipTLSVerify) - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + "host_records/" + name + "/" - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return errors.Wrapf(err, "error building http request: %v", name) - } - resp, err := client.Do(req) + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) if err != nil { return errors.Wrapf(err, "error retrieving host record %v from gateway", name) } - defer resp.Body.Close() + defer response.Body.Close() - if resp.StatusCode != http.StatusOK { - return errors.Errorf("received http %v while retrieving host record %v from gateway", resp.StatusCode, name) + if response.StatusCode != http.StatusOK { + return errors.Errorf("received http %v while retrieving host record %v from gateway", response.StatusCode, name) } - json.NewDecoder(resp.Body).Decode(record) + json.NewDecoder(response.Body).Decode(record) log.Debugf("Get Host Record Response: %v", record) return nil } func (c GatewayClientConfig) GetCNAMERecord(name string, record *BluecatCNAMERecord) error { - client := newHTTPClient(c.SkipTLSVerify) - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + "cname_records/" + name + "/" - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return errors.Wrapf(err, "error building http request: %v", name) - } - resp, err := client.Do(req) + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) if err != nil { return errors.Wrapf(err, "error retrieving cname record %v from gateway", name) } - defer resp.Body.Close() + defer response.Body.Close() - if resp.StatusCode != http.StatusOK { - return errors.Errorf("received http %v while retrieving cname record %v from gateway", resp.StatusCode, name) + if response.StatusCode != http.StatusOK { + return errors.Errorf("received http %v while retrieving cname record %v from gateway", response.StatusCode, name) } - json.NewDecoder(resp.Body).Decode(record) + json.NewDecoder(response.Body).Decode(record) log.Debugf("Get CName Record Response: %v", record) return nil } func (c GatewayClientConfig) GetTXTRecord(name string, record *BluecatTXTRecord) error { - client := newHTTPClient(c.SkipTLSVerify) - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + "text_records/" + name + "/" - req, err := c.buildHTTPRequest("GET", url, nil) - if err != nil { - return errors.Wrap(err, "error building http request") - } - - resp, err := client.Do(req) + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) if err != nil { return errors.Wrapf(err, "error retrieving record %v from gateway", name) } - defer resp.Body.Close() + defer response.Body.Close() - if resp.StatusCode != http.StatusOK { - return errors.Errorf("received http %v while retrieving txt record %v from gateway", resp.StatusCode, name) + if response.StatusCode != http.StatusOK { + return errors.Errorf("received http %v while retrieving txt record %v from gateway", response.StatusCode, name) } - json.NewDecoder(resp.Body).Decode(record) + json.NewDecoder(response.Body).Decode(record) log.Debugf("Get TXT Record Response: %v", record) return nil } func (c GatewayClientConfig) CreateHostRecord(zone string, req *BluecatCreateHostRecordRequest) error { - client := newHTTPClient(c.SkipTLSVerify) - zonePath := expandZone(zone) // Remove the trailing 'zones/' zonePath = strings.TrimSuffix(zonePath, "zones/") url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/" - body, _ := json.Marshal(req) - hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) + body, err := json.Marshal(req) if err != nil { - return errors.Wrap(err, "error building http request") + return errors.Wrap(err, "could not marshal body for create host record") } - hreq.Header.Add("Content-Type", "application/json") - resp, err := client.Do(hreq) + + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie) if err != nil { return errors.Wrapf(err, "error creating host record %v in gateway", req.AbsoluteName) } - defer resp.Body.Close() + defer response.Body.Close() - if resp.StatusCode != http.StatusCreated { - return errors.Errorf("received http %v while creating host record %v in gateway", resp.StatusCode, req.AbsoluteName) + if response.StatusCode != http.StatusCreated { + return errors.Errorf("received http %v while creating host record %v in gateway", response.StatusCode, req.AbsoluteName) } return nil } func (c GatewayClientConfig) CreateCNAMERecord(zone string, req *BluecatCreateCNAMERecordRequest) error { - client := newHTTPClient(c.SkipTLSVerify) - zonePath := expandZone(zone) // Remove the trailing 'zones/' zonePath = strings.TrimSuffix(zonePath, "zones/") url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/" - body, _ := json.Marshal(req) - - hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) + body, err := json.Marshal(req) if err != nil { - return errors.Wrap(err, "error building http request") + return errors.Wrap(err, "could not marshal body for create cname record") } - hreq.Header.Add("Content-Type", "application/json") - resp, err := client.Do(hreq) + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie) if err != nil { return errors.Wrapf(err, "error creating cname record %v in gateway", req.AbsoluteName) } - defer resp.Body.Close() + defer response.Body.Close() - if resp.StatusCode != http.StatusCreated { - return errors.Errorf("received http %v while creating cname record %v to alias %v in gateway", resp.StatusCode, req.AbsoluteName, req.LinkedRecord) + if response.StatusCode != http.StatusCreated { + return errors.Errorf("received http %v while creating cname record %v to alias %v in gateway", response.StatusCode, req.AbsoluteName, req.LinkedRecord) } return nil } func (c GatewayClientConfig) CreateTXTRecord(zone string, req *BluecatCreateTXTRecordRequest) error { - client := newHTTPClient(c.SkipTLSVerify) - zonePath := expandZone(zone) // Remove the trailing 'zones/' zonePath = strings.TrimSuffix(zonePath, "zones/") url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/" - body, _ := json.Marshal(req) - hreq, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) + body, err := json.Marshal(req) if err != nil { - return errors.Wrap(err, "error building http request") + return errors.Wrap(err, "could not marshal body for create txt record") } - hreq.Header.Add("Content-Type", "application/json") - resp, err := client.Do(hreq) + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie) if err != nil { return errors.Wrapf(err, "error creating txt record %v in gateway", req.AbsoluteName) } - defer resp.Body.Close() + defer response.Body.Close() - if resp.StatusCode != http.StatusCreated { - return errors.Errorf("received http %v while creating txt record %v in gateway", resp.StatusCode, req.AbsoluteName) + if response.StatusCode != http.StatusCreated { + return errors.Errorf("received http %v while creating txt record %v in gateway", response.StatusCode, req.AbsoluteName) } return nil } func (c GatewayClientConfig) DeleteHostRecord(name string, zone string) (err error) { - client := newHTTPClient(c.SkipTLSVerify) - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + "host_records/" + name + "." + zone + "/" - req, err := c.buildHTTPRequest("DELETE", url, nil) - if err != nil { - return errors.Wrapf(err, "error building http request: %v", name) - } - resp, err := client.Do(req) + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodDelete, url, c.Token, nil, c.Cookie) if err != nil { return errors.Wrapf(err, "error deleting host record %v from gateway", name) } - if resp.StatusCode != http.StatusNoContent { - return errors.Errorf("received http %v while deleting host record %v from gateway", resp.StatusCode, name) + if response.StatusCode != http.StatusNoContent { + return errors.Errorf("received http %v while deleting host record %v from gateway", response.StatusCode, name) } return nil } func (c GatewayClientConfig) DeleteCNAMERecord(name string, zone string) (err error) { - client := newHTTPClient(c.SkipTLSVerify) - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + "cname_records/" + name + "." + zone + "/" - req, err := c.buildHTTPRequest("DELETE", url, nil) - if err != nil { - return errors.Wrapf(err, "error building http request: %v", name) - } - resp, err := client.Do(req) + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodDelete, url, c.Token, nil, c.Cookie) if err != nil { return errors.Wrapf(err, "error deleting cname record %v from gateway", name) } - - if resp.StatusCode != http.StatusNoContent { - return errors.Errorf("received http %v while deleting cname record %v from gateway", resp.StatusCode, name) + if response.StatusCode != http.StatusNoContent { + return errors.Errorf("received http %v while deleting cname record %v from gateway", response.StatusCode, name) } return nil } func (c GatewayClientConfig) DeleteTXTRecord(name string, zone string) error { - client := newHTTPClient(c.SkipTLSVerify) - url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + "text_records/" + name + "." + zone + "/" - req, err := c.buildHTTPRequest("DELETE", url, nil) - if err != nil { - return errors.Wrap(err, "error building http request") - } - - resp, err := client.Do(req) + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodDelete, url, c.Token, nil, c.Cookie) if err != nil { return errors.Wrapf(err, "error deleting txt record %v from gateway", name) } - - if resp.StatusCode != http.StatusNoContent { - return errors.Errorf("received http %v while deleting txt record %v from gateway", resp.StatusCode, name) + if response.StatusCode != http.StatusNoContent { + return errors.Errorf("received http %v while deleting txt record %v from gateway", response.StatusCode, name) } return nil @@ -569,7 +489,6 @@ func (c GatewayClientConfig) DeleteTXTRecord(name string, zone string) error { func (c GatewayClientConfig) ServerFullDeploy() error { log.Infof("Executing full deploy on server %s", c.DNSServerName) - httpClient := newHTTPClient(c.SkipTLSVerify) url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/server/full_deploy/" requestBody := BluecatServerFullDeployRequest{ ServerName: c.DNSServerName, @@ -580,13 +499,7 @@ func (c GatewayClientConfig) ServerFullDeploy() error { return errors.Wrap(err, "could not marshal body for server full deploy") } - request, err := c.buildHTTPRequest("POST", url, bytes.NewBuffer(body)) - if err != nil { - return errors.Wrap(err, "error building http request") - } - - request.Header.Add("Content-Type", "application/json") - response, err := httpClient.Do(request) + response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie) if err != nil { return errors.Wrap(err, "error executing full deploy") } @@ -602,15 +515,6 @@ func (c GatewayClientConfig) ServerFullDeploy() error { return nil } -// buildHTTPRequest builds a standard http Request and adds authentication headers required by Bluecat Gateway -func (c GatewayClientConfig) buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error) { - req, err := http.NewRequest(method, url, body) - req.Header.Add("Accept", "application/json") - req.Header.Add("Authorization", "Basic "+c.Token) - req.AddCookie(&c.Cookie) - return req, err -} - // SplitProperties is a helper function to break a '|' separated string into key/value pairs // i.e. "foo=bar|baz=mop" func SplitProperties(props string) map[string]string { @@ -654,9 +558,8 @@ func expandZone(zone string) string { return ze } -// newHTTPClient returns an instance of http client -func newHTTPClient(skipTLSVerify bool) *http.Client { - return &http.Client{ +func executeHTTPRequest(skipTLSVerify bool, method, url, token string, body io.Reader, cookie http.Cookie) (*http.Response, error) { + httpClient := &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{ @@ -664,4 +567,19 @@ func newHTTPClient(skipTLSVerify bool) *http.Client { }, }, } + request, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + if request.Method == http.MethodPost { + request.Header.Add("Content-Type", "application/json") + } + request.Header.Add("Accept", "application/json") + + if token != "" { + request.Header.Add("Authorization", "Basic "+token) + } + request.AddCookie(&cookie) + + return httpClient.Do(request) } From 0d16c66dbe6eed4459884438df439efb448ba715 Mon Sep 17 00:00:00 2001 From: Vinny Sabatini Date: Fri, 25 Mar 2022 17:14:26 -0500 Subject: [PATCH 7/8] provider/bluecat: minor test cleanup * Move tests to appropriate package * Add test for SplitProperties function * Remove unused helper function * Use table driven tests --- provider/bluecat/bluecat_test.go | 24 ---------- provider/bluecat/gateway/api_test.go | 67 ++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 32 deletions(-) diff --git a/provider/bluecat/bluecat_test.go b/provider/bluecat/bluecat_test.go index a877145e5..a5cc8787b 100644 --- a/provider/bluecat/bluecat_test.go +++ b/provider/bluecat/bluecat_test.go @@ -16,8 +16,6 @@ package bluecat import ( "context" "fmt" - "io" - "net/http" "strings" "testing" @@ -114,11 +112,6 @@ func (g mockGatewayClient) ServerFullDeploy() error { return nil } -func (g mockGatewayClient) buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error) { - request, _ := http.NewRequest("GET", fmt.Sprintf("%s/users", "http://some.com/api/v1"), nil) - return request, nil -} - func createMockBluecatZone(fqdn string) api.BluecatZone { props := "absoluteName=" + fqdn return api.BluecatZone{ @@ -364,23 +357,6 @@ func TestBluecatApplyChangesDeleteWithOwner(t *testing.T) { } -func TestBluecatNewGatewayClient(t *testing.T) { - testCookie := http.Cookie{Name: "testCookie", Value: "exampleCookie"} - testToken := "exampleToken" - testgateWayHost := "exampleHost" - testDNSConfiguration := "exampleDNSConfiguration" - testDNSServer := "exampleServer" - testView := "testView" - testZone := "example.com" - testVerify := true - - client := api.NewGatewayClientConfig(testCookie, testToken, testgateWayHost, testDNSConfiguration, testView, testZone, testDNSServer, testVerify) - - if client.Cookie.Value != testCookie.Value || client.Cookie.Name != testCookie.Name || client.Token != testToken || client.Host != testgateWayHost || client.DNSConfiguration != testDNSConfiguration || client.View != testView || client.RootZone != testZone || client.SkipTLSVerify != testVerify { - t.Fatal("Client values dont match") - } -} - // TODO: ensure findZone method is tested // TODO: ensure zones method is tested // TODO: ensure createRecords method is tested diff --git a/provider/bluecat/gateway/api_test.go b/provider/bluecat/gateway/api_test.go index 68142a9c6..76765f906 100644 --- a/provider/bluecat/gateway/api_test.go +++ b/provider/bluecat/gateway/api_test.go @@ -14,20 +14,50 @@ limitations under the License. package api import ( + "net/http" "testing" + + "github.com/google/go-cmp/cmp" ) -func TestExpandZones(t *testing.T) { - mockZones := []string{"example.com", "nginx.example.com", "hack.example.com"} - expected := []string{"zones/com/zones/example/zones/", "zones/com/zones/example/zones/nginx/zones/", "zones/com/zones/example/zones/hack/zones/"} - for i := range mockZones { - if expandZone(mockZones[i]) != expected[i] { - t.Fatalf("%s", expected[i]) - } +func TestBluecatNewGatewayClient(t *testing.T) { + testCookie := http.Cookie{Name: "testCookie", Value: "exampleCookie"} + testToken := "exampleToken" + testgateWayHost := "exampleHost" + testDNSConfiguration := "exampleDNSConfiguration" + testDNSServer := "exampleServer" + testView := "testView" + testZone := "example.com" + testVerify := true + + client := NewGatewayClientConfig(testCookie, testToken, testgateWayHost, testDNSConfiguration, testView, testZone, testDNSServer, testVerify) + + if client.Cookie.Value != testCookie.Value || client.Cookie.Name != testCookie.Name || client.Token != testToken || client.Host != testgateWayHost || client.DNSConfiguration != testDNSConfiguration || client.View != testView || client.RootZone != testZone || client.SkipTLSVerify != testVerify { + t.Fatal("Client values dont match") } } -func TestValidDeployTypes(t *testing.T) { +func TestBluecatExpandZones(t *testing.T) { + tests := map[string]struct { + input string + want string + }{ + "with subdomain": {input: "example.com", want: "zones/com/zones/example/zones/"}, + "only top level domain": {input: "com", want: "zones/com/zones/"}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := expandZone(tc.input) + diff := cmp.Diff(tc.want, got) + if diff != "" { + t.Fatalf(diff) + } + }) + } +} + +func TestBluecatValidDeployTypes(t *testing.T) { validTypes := []string{"no-deploy", "full-deploy"} invalidTypes := []string{"anything-else"} for _, i := range validTypes { @@ -41,3 +71,24 @@ func TestValidDeployTypes(t *testing.T) { } } } + +// TODO: Add error checking in case "properties" are not properly formatted +// Example test case... "invalid": {input: "abcde", want: map[string]string{}, err: InvalidProperty}, +func TestBluecatSplitProperties(t *testing.T) { + tests := map[string]struct { + input string + want map[string]string + }{ + "simple": {input: "ab=cd|ef=gh", want: map[string]string{"ab": "cd", "ef": "gh"}}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := SplitProperties(tc.input) + diff := cmp.Diff(tc.want, got) + if diff != "" { + t.Fatalf(diff) + } + }) + } +} From d4927b2a54e1e8c38e68b30e2fa9dffb2dbbaebe Mon Sep 17 00:00:00 2001 From: Vinny Sabatini Date: Fri, 8 Apr 2022 15:21:02 -0500 Subject: [PATCH 8/8] provider/bluecat: first set of tests for api --- provider/bluecat/gateway/api_test.go | 134 +++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/provider/bluecat/gateway/api_test.go b/provider/bluecat/gateway/api_test.go index 76765f906..88aad8326 100644 --- a/provider/bluecat/gateway/api_test.go +++ b/provider/bluecat/gateway/api_test.go @@ -14,7 +14,11 @@ limitations under the License. package api import ( + "encoding/json" + "io/ioutil" "net/http" + "net/http/httptest" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -92,3 +96,133 @@ func TestBluecatSplitProperties(t *testing.T) { }) } } + +func TestCreateTXTRecord(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + req := BluecatCreateTXTRecordRequest{} + requestBodyBytes, _ := ioutil.ReadAll(r.Body) + err := json.Unmarshal(requestBodyBytes, &req) + if err != nil { + t.Fatalf("failed to unmarshal body for server full deploy") + } + if req.AbsoluteName == "alreadyexists.test.com" { + w.WriteHeader(http.StatusInternalServerError) + } else { + w.WriteHeader(http.StatusCreated) + } + })) + defer server.Close() + + tests := map[string]struct { + config GatewayClientConfig + zone string + record BluecatCreateTXTRecordRequest + expectError bool + }{ + "simple-success": {GatewayClientConfig{Host: server.URL}, "test.com", BluecatCreateTXTRecordRequest{AbsoluteName: "my.test.com", Text: "here is my text"}, false}, + "simple-failure": {GatewayClientConfig{Host: server.URL}, "test.com", BluecatCreateTXTRecordRequest{AbsoluteName: "alreadyexists.test.com", Text: "here is my text"}, true}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := tc.config.CreateTXTRecord(tc.zone, &tc.record) + if got != nil && !tc.expectError { + t.Fatalf("expected error %v, received error %v", tc.expectError, got) + } + }) + } +} + +func TestGetTXTRecord(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.RequestURI, "doesnotexist") { + w.WriteHeader(http.StatusNotFound) + } else { + w.WriteHeader(http.StatusOK) + } + })) + defer server.Close() + + tests := map[string]struct { + config GatewayClientConfig + name string + expectError bool + }{ + "simple-success": {GatewayClientConfig{Host: server.URL}, "mytxtrecord", false}, + "simple-failure": {GatewayClientConfig{Host: server.URL}, "doesnotexist", true}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + record := BluecatTXTRecord{} + got := tc.config.GetTXTRecord(tc.name, &record) + if got != nil && !tc.expectError { + t.Fatalf("expected error %v, received error %v", tc.expectError, got) + } + }) + } +} + +func TestDeleteTXTRecord(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.RequestURI, "doesnotexist") { + w.WriteHeader(http.StatusBadRequest) + } else { + w.WriteHeader(http.StatusNoContent) + } + })) + defer server.Close() + + tests := map[string]struct { + config GatewayClientConfig + name string + zone string + expectError bool + }{ + "simple-success": {GatewayClientConfig{Host: server.URL}, "todelete", "test.com", false}, + "simple-failure": {GatewayClientConfig{Host: server.URL}, "doesnotexist", "test.com", true}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := tc.config.DeleteTXTRecord(tc.name, tc.zone) + if got != nil && !tc.expectError { + t.Fatalf("expected error %v, received error %v", tc.expectError, got) + } + }) + } +} + +func TestServerFullDeploy(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + req := BluecatServerFullDeployRequest{} + requestBodyBytes, _ := ioutil.ReadAll(r.Body) + err := json.Unmarshal(requestBodyBytes, &req) + if err != nil { + t.Fatalf("failed to unmarshal body for server full deploy") + } + if req.ServerName == "serverdoesnotexist" { + w.WriteHeader(http.StatusNotFound) + } else { + w.WriteHeader(http.StatusCreated) + } + })) + defer server.Close() + + tests := map[string]struct { + config GatewayClientConfig + expectError bool + }{ + "simple-success": {GatewayClientConfig{Host: server.URL, DNSServerName: "myserver"}, false}, + "simple-failure": {GatewayClientConfig{Host: server.URL, DNSServerName: "serverdoesnotexist"}, true}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := tc.config.ServerFullDeploy() + if got != nil && !tc.expectError { + t.Fatalf("expected error %v, received error %v", tc.expectError, got) + } + }) + } +}