Merge pull request #2696 from KohlsTechnology/issue-2187

extract bluecat api code into its own package
This commit is contained in:
Kubernetes Prow Robot 2022-04-12 09:45:24 -07:00 committed by GitHub
commit 1dcc89eea4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 935 additions and 738 deletions

View File

@ -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":

View File

@ -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,
},
},
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
})
}
}