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 e54a84e65..2c25406fb 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,9 @@ 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 + TxtPrefix string + TxtSuffix string } type bluecatRecordSet struct { @@ -131,38 +60,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{} +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 { if errors.Is(err, os.ErrNotExist) { - cfg = bluecatConfig{ + cfg = api.BluecatConfig{ GatewayHost: gatewayHost, DNSConfiguration: dnsConfiguration, DNSServerName: dnsServerName, @@ -183,15 +89,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, @@ -203,31 +109,12 @@ func NewBluecatProvider(configFile, dnsConfiguration, dnsServerName, dnsDeployTy DNSDeployType: cfg.DNSDeployType, View: cfg.View, RootZone: cfg.RootZone, + TxtPrefix: txtPrefix, + TxtSuffix: txtSuffix, } 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 +126,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 +140,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 { @@ -273,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) @@ -284,14 +171,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 { @@ -302,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) @@ -334,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") @@ -404,7 +295,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 +310,7 @@ func (p *BluecatProvider) zones() ([]string, error) { continue } - zoneProps := splitProperties(zone.Properties) + zoneProps := api.SplitProperties(zone.Properties) zones = append(zones, zoneProps["absoluteName"]) } @@ -462,11 +353,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)) + err = p.gatewayClient.CreateHostRecord(zone, recordSet.obj.(*api.BluecatCreateHostRecordRequest)) case endpoint.RecordTypeCNAME: - response, err = p.gatewayClient.createCNAMERecord(zone, recordSet.obj.(*bluecatCreateCNAMERecordRequest)) + err = p.gatewayClient.CreateCNAMERecord(zone, recordSet.obj.(*api.BluecatCreateCNAMERecordRequest)) case endpoint.RecordTypeTXT: - response, err = p.gatewayClient.createTXTRecord(zone, recordSet.obj.(*bluecatCreateTXTRecordRequest)) + err = p.gatewayClient.CreateTXTRecord(zone, recordSet.obj.(*api.BluecatCreateTXTRecordRequest)) } log.Debugf("Response from create: %v", response) if err != nil { @@ -516,16 +407,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 +434,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 +454,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 +474,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 +497,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 +509,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..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" @@ -26,13 +24,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 +45,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 +65,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 +74,25 @@ func (g mockGatewayClient) getCNAMERecord(name string, record *BluecatCNAMERecor } return nil } -func (g mockGatewayClient) createHostRecord(zone string, req *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 *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) { +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,58 +101,53 @@ func (g mockGatewayClient) getTXTRecord(name string, record *BluecatTXTRecord) e } return nil } -func (g mockGatewayClient) createTXTRecord(zone string, req *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 { +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 } -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) 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 +213,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 +246,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 +273,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 +311,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", ""), @@ -344,9 +338,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}) @@ -362,33 +357,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" - testgateWayHost := "exampleHost" - testDNSConfiguration := "exampleDNSConfiguration" - testDNSServer := "exampleServer" - testView := "testView" - testZone := "example.com" - testVerify := true - - client := NewGatewayClient(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 @@ -398,18 +366,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 +388,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 +408,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 +428,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 +447,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..2f4389540 --- /dev/null +++ b/provider/bluecat/gateway/api.go @@ -0,0 +1,585 @@ +/* +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. +*/ +// TODO: add logging +// TODO: add timeouts +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) 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) 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") + } + url := cfg.GatewayHost + "/rest_login" + + response, err := executeHTTPRequest(cfg.SkipTLSVerify, http.MethodPost, url, "", bytes.NewBuffer(body), http.Cookie{}) + + if err != nil { + return "", http.Cookie{}, errors.Wrap(err, "error obtaining API token from bluecat gateway") + } + defer response.Body.Close() + + responseBody, err := ioutil.ReadAll(response.Body) + if err != nil { + return "", http.Cookie{}, errors.Wrap(err, "failed to read login response from bluecat gateway") + } + + 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") + } + + // 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(jsonResponse["access_token"], " ")[1], *response.Cookies()[0], nil +} + +func (c GatewayClientConfig) GetBluecatZones(zoneName string) ([]BluecatZone, error) { + zonePath := expandZone(zoneName) + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + + 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 response.Body.Close() + + 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(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 + 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 { + 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/" + + 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 response.Body.Close() + + if response.StatusCode != http.StatusOK { + return errors.Errorf("received http %v requesting host records from gateway in zone %v", response.StatusCode, zone) + } + + 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 { + 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/" + + 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 response.Body.Close() + + if response.StatusCode != http.StatusOK { + return errors.Errorf("received http %v requesting cname records from gateway in zone %v", response.StatusCode, zone) + } + + 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 { + 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/" + + 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 response.Body.Close() + + 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", 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 { + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + + "/views/" + c.View + "/" + + "host_records/" + name + "/" + + 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 response.Body.Close() + + if response.StatusCode != http.StatusOK { + return errors.Errorf("received http %v while retrieving host record %v from gateway", response.StatusCode, name) + } + + 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 { + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + + "/views/" + c.View + "/" + + "cname_records/" + name + "/" + + 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 response.Body.Close() + + if response.StatusCode != http.StatusOK { + return errors.Errorf("received http %v while retrieving cname record %v from gateway", response.StatusCode, name) + } + + 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 { + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + + "/views/" + c.View + "/" + + "text_records/" + name + "/" + + 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 response.Body.Close() + + if response.StatusCode != http.StatusOK { + return errors.Errorf("received http %v while retrieving txt record %v from gateway", response.StatusCode, name) + } + + 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 { + 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, err := json.Marshal(req) + if err != nil { + return errors.Wrap(err, "could not marshal body for create host record") + } + + 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 response.Body.Close() + + 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 { + 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, err := json.Marshal(req) + if err != nil { + return errors.Wrap(err, "could not marshal body for create cname record") + } + + 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 response.Body.Close() + + 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 { + 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, err := json.Marshal(req) + if err != nil { + return errors.Wrap(err, "could not marshal body for create txt record") + } + + 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 response.Body.Close() + + 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) { + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + + "/views/" + c.View + "/" + + "host_records/" + name + "." + zone + "/" + + 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 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) { + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + + "/views/" + c.View + "/" + + "cname_records/" + name + "." + zone + "/" + + 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 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 { + url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + + "/views/" + c.View + "/" + + "text_records/" + name + "." + zone + "/" + + 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 response.StatusCode != http.StatusNoContent { + return errors.Errorf("received http %v while deleting txt record %v from gateway", response.StatusCode, name) + } + + return nil +} + +func (c GatewayClientConfig) ServerFullDeploy() error { + log.Infof("Executing full deploy on server %s", c.DNSServerName) + 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") + } + + 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") + } + + 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 +} + +// 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 +} + +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{ + InsecureSkipVerify: skipTLSVerify, + }, + }, + } + 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) +} diff --git a/provider/bluecat/gateway/api_test.go b/provider/bluecat/gateway/api_test.go new file mode 100644 index 000000000..88aad8326 --- /dev/null +++ b/provider/bluecat/gateway/api_test.go @@ -0,0 +1,228 @@ +/* +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 ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +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 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 { + 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) + } + } +} + +// 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) + } + }) + } +} + +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) + } + }) + } +}