provider/bluecat: add full deploy functionality

New configuration options created for setting the DNS deployment type,
as well as the DNS server to deploy. A DNS server name must be provided
and a valid DNS deployment type must be set in order for a deployment to be
initiated.

Currently, the only supported deployment type is "full deploy", however
"quick deploy" and "selective deploy" could be added in the future.
This commit is contained in:
Vinny Sabatini 2022-02-04 17:20:04 -06:00
parent 261bcadd7c
commit 8aef3e089f
6 changed files with 118 additions and 7 deletions

View File

@ -82,6 +82,8 @@ The options for configuring the Bluecat Provider are available through the JSON
| dnsConfiguration | Yes | | dnsConfiguration | Yes |
| dnsView | Yes | | dnsView | Yes |
| rootZone | Yes | | rootZone | Yes |
| dnsServerName | No |
| dnsDeployType | No |
| skipTLSVerify | No (default false) | | skipTLSVerify | No (default false) |
#### Deploy #### Deploy

View File

@ -212,7 +212,7 @@ func main() {
case "azure-private-dns": case "azure-private-dns":
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun) p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "bluecat": case "bluecat":
p, err = bluecat.NewBluecatProvider(cfg.BluecatConfigFile, cfg.BluecatDNSConfiguration, 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, domainFilter, zoneIDFilter, cfg.DryRun, cfg.BluecatSkipTLSVerify)
case "vinyldns": case "vinyldns":
p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun) p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "vultr": case "vultr":

View File

@ -96,6 +96,8 @@ type Config struct {
BluecatDNSView string BluecatDNSView string
BluecatGatewayHost string BluecatGatewayHost string
BluecatRootZone string BluecatRootZone string
BluecatDNSServerName string
BluecatDNSDeployType string
BluecatSkipTLSVerify bool BluecatSkipTLSVerify bool
CloudflareProxied bool CloudflareProxied bool
CloudflareZonesPerPage int CloudflareZonesPerPage int
@ -228,6 +230,7 @@ var defaultConfig = &Config{
AzureResourceGroup: "", AzureResourceGroup: "",
AzureSubscriptionID: "", AzureSubscriptionID: "",
BluecatConfigFile: "/etc/kubernetes/bluecat.json", BluecatConfigFile: "/etc/kubernetes/bluecat.json",
BluecatDNSDeployType: "no-deploy",
CloudflareProxied: false, CloudflareProxied: false,
CloudflareZonesPerPage: 50, CloudflareZonesPerPage: 50,
CoreDNSPrefix: "/skydns/", CoreDNSPrefix: "/skydns/",
@ -426,6 +429,8 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("bluecat-gateway-host", "When using the Bluecat provider, specify the Bluecat Gateway Host (optional when --provider=bluecat)").Default("").StringVar(&cfg.BluecatGatewayHost) app.Flag("bluecat-gateway-host", "When using the Bluecat provider, specify the Bluecat Gateway Host (optional when --provider=bluecat)").Default("").StringVar(&cfg.BluecatGatewayHost)
app.Flag("bluecat-root-zone", "When using the Bluecat provider, specify the Bluecat root zone (optional when --provider=bluecat)").Default("").StringVar(&cfg.BluecatRootZone) app.Flag("bluecat-root-zone", "When using the Bluecat provider, specify the Bluecat root zone (optional when --provider=bluecat)").Default("").StringVar(&cfg.BluecatRootZone)
app.Flag("bluecat-skip-tls-verify", "When using the Bluecat provider, specify to skip TLS verification (optional when --provider=bluecat) (default: false)").BoolVar(&cfg.BluecatSkipTLSVerify) app.Flag("bluecat-skip-tls-verify", "When using the Bluecat provider, specify to skip TLS verification (optional when --provider=bluecat) (default: false)").BoolVar(&cfg.BluecatSkipTLSVerify)
app.Flag("bluecat-dns-server-name", "When using the Bluecat provider, specify the Bluecat DNS Server to initiate deploys against. This is only used if --bluecat-dns-deploy-type is not 'no-deploy' (optional when --provider=bluecat)").Default("").StringVar(&cfg.BluecatDNSServerName)
app.Flag("bluecat-dns-deploy-type", "When using the Bluecat provider, specify the type of DNS deployment to initiate after records are updated. Valid options are 'full-deploy' and 'no-deploy'. Deploy will only execute if --bluecat-dns-server-name is set (optional when --provider=bluecat)").Default(defaultConfig.BluecatDNSDeployType).StringVar(&cfg.BluecatDNSDeployType)
app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied) app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied)
app.Flag("cloudflare-zones-per-page", "When using the Cloudflare provider, specify how many zones per page listed, max. possible 50 (default: 50)").Default(strconv.Itoa(defaultConfig.CloudflareZonesPerPage)).IntVar(&cfg.CloudflareZonesPerPage) app.Flag("cloudflare-zones-per-page", "When using the Cloudflare provider, specify how many zones per page listed, max. possible 50 (default: 50)").Default(strconv.Itoa(defaultConfig.CloudflareZonesPerPage)).IntVar(&cfg.CloudflareZonesPerPage)

View File

@ -67,10 +67,12 @@ var (
AzureResourceGroup: "", AzureResourceGroup: "",
AzureSubscriptionID: "", AzureSubscriptionID: "",
BluecatDNSConfiguration: "", BluecatDNSConfiguration: "",
BluecatDNSServerName: "",
BluecatConfigFile: "/etc/kubernetes/bluecat.json", BluecatConfigFile: "/etc/kubernetes/bluecat.json",
BluecatDNSView: "", BluecatDNSView: "",
BluecatGatewayHost: "", BluecatGatewayHost: "",
BluecatRootZone: "", BluecatRootZone: "",
BluecatDNSDeployType: defaultConfig.BluecatDNSDeployType,
BluecatSkipTLSVerify: false, BluecatSkipTLSVerify: false,
CloudflareProxied: false, CloudflareProxied: false,
CloudflareZonesPerPage: 50, CloudflareZonesPerPage: 50,
@ -162,10 +164,12 @@ var (
AzureResourceGroup: "arg", AzureResourceGroup: "arg",
AzureSubscriptionID: "arg", AzureSubscriptionID: "arg",
BluecatDNSConfiguration: "arg", BluecatDNSConfiguration: "arg",
BluecatDNSServerName: "arg",
BluecatConfigFile: "bluecat.json", BluecatConfigFile: "bluecat.json",
BluecatDNSView: "arg", BluecatDNSView: "arg",
BluecatGatewayHost: "arg", BluecatGatewayHost: "arg",
BluecatRootZone: "arg", BluecatRootZone: "arg",
BluecatDNSDeployType: "full-deploy",
BluecatSkipTLSVerify: true, BluecatSkipTLSVerify: true,
CloudflareProxied: true, CloudflareProxied: true,
CloudflareZonesPerPage: 20, CloudflareZonesPerPage: 20,
@ -270,8 +274,10 @@ func TestParseFlags(t *testing.T) {
"--bluecat-dns-configuration=arg", "--bluecat-dns-configuration=arg",
"--bluecat-config-file=bluecat.json", "--bluecat-config-file=bluecat.json",
"--bluecat-dns-view=arg", "--bluecat-dns-view=arg",
"--bluecat-dns-server-name=arg",
"--bluecat-gateway-host=arg", "--bluecat-gateway-host=arg",
"--bluecat-root-zone=arg", "--bluecat-root-zone=arg",
"--bluecat-dns-deploy-type=full-deploy",
"--bluecat-skip-tls-verify", "--bluecat-skip-tls-verify",
"--cloudflare-proxied", "--cloudflare-proxied",
"--cloudflare-zones-per-page=20", "--cloudflare-zones-per-page=20",
@ -379,6 +385,8 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg", "EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg",
"EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg", "EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "arg",
"EXTERNAL_DNS_BLUECAT_DNS_CONFIGURATION": "arg", "EXTERNAL_DNS_BLUECAT_DNS_CONFIGURATION": "arg",
"EXTERNAL_DNS_BLUECAT_DNS_SERVER_NAME": "arg",
"EXTERNAL_DNS_BLUECAT_DNS_DEPLOY_TYPE": "full-deploy",
"EXTERNAL_DNS_BLUECAT_CONFIG_FILE": "bluecat.json", "EXTERNAL_DNS_BLUECAT_CONFIG_FILE": "bluecat.json",
"EXTERNAL_DNS_BLUECAT_DNS_VIEW": "arg", "EXTERNAL_DNS_BLUECAT_DNS_VIEW": "arg",
"EXTERNAL_DNS_BLUECAT_GATEWAY_HOST": "arg", "EXTERNAL_DNS_BLUECAT_GATEWAY_HOST": "arg",

View File

@ -44,6 +44,8 @@ type bluecatConfig struct {
GatewayUsername string `json:"gatewayUsername,omitempty"` GatewayUsername string `json:"gatewayUsername,omitempty"`
GatewayPassword string `json:"gatewayPassword,omitempty"` GatewayPassword string `json:"gatewayPassword,omitempty"`
DNSConfiguration string `json:"dnsConfiguration"` DNSConfiguration string `json:"dnsConfiguration"`
DNSServerName string `json:"dnsServerName"`
DNSDeployType string `json:"dnsDeployType"`
View string `json:"dnsView"` View string `json:"dnsView"`
RootZone string `json:"rootZone"` RootZone string `json:"rootZone"`
SkipTLSVerify bool `json:"skipTLSVerify"` SkipTLSVerify bool `json:"skipTLSVerify"`
@ -57,6 +59,8 @@ type BluecatProvider struct {
dryRun bool dryRun bool
RootZone string RootZone string
DNSConfiguration string DNSConfiguration string
DNSServerName string
DNSDeployType string
View string View string
gatewayClient GatewayClient gatewayClient GatewayClient
} }
@ -76,6 +80,7 @@ type GatewayClient interface {
getTXTRecord(name string, record *BluecatTXTRecord) error getTXTRecord(name string, record *BluecatTXTRecord) error
createTXTRecord(zone string, req *bluecatCreateTXTRecordRequest) (res interface{}, err error) createTXTRecord(zone string, req *bluecatCreateTXTRecordRequest) (res interface{}, err error)
deleteTXTRecord(name string, zone string) error deleteTXTRecord(name string, zone string) error
serverFullDeploy() error
} }
// GatewayClientConfig defines new client on bluecat gateway // GatewayClientConfig defines new client on bluecat gateway
@ -86,6 +91,7 @@ type GatewayClientConfig struct {
DNSConfiguration string DNSConfiguration string
View string View string
RootZone string RootZone string
DNSServerName string
SkipTLSVerify bool SkipTLSVerify bool
} }
@ -144,10 +150,14 @@ type bluecatCreateTXTRecordRequest struct {
Text string `json:"txt"` Text string `json:"txt"`
} }
type bluecatServerFullDeployRequest struct {
ServerName string `json:"server_name"`
}
// NewBluecatProvider creates a new Bluecat provider. // NewBluecatProvider creates a new Bluecat provider.
// //
// Returns a pointer to the provider or an error if a provider could not be created. // Returns a pointer to the provider or an error if a provider could not be created.
func NewBluecatProvider(configFile, dnsConfiguration, dnsView, gatewayHost, rootZone string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun, skipTLSVerify bool) (*BluecatProvider, error) { func NewBluecatProvider(configFile, dnsConfiguration, dnsServerName, dnsDeployType, dnsView, gatewayHost, rootZone string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun, skipTLSVerify bool) (*BluecatProvider, error) {
cfg := bluecatConfig{} cfg := bluecatConfig{}
contents, err := os.ReadFile(configFile) contents, err := os.ReadFile(configFile)
if err != nil { if err != nil {
@ -155,6 +165,8 @@ func NewBluecatProvider(configFile, dnsConfiguration, dnsView, gatewayHost, root
cfg = bluecatConfig{ cfg = bluecatConfig{
GatewayHost: gatewayHost, GatewayHost: gatewayHost,
DNSConfiguration: dnsConfiguration, DNSConfiguration: dnsConfiguration,
DNSServerName: dnsServerName,
DNSDeployType: dnsDeployType,
View: dnsView, View: dnsView,
RootZone: rootZone, RootZone: rootZone,
SkipTLSVerify: skipTLSVerify, SkipTLSVerify: skipTLSVerify,
@ -171,11 +183,15 @@ func NewBluecatProvider(configFile, dnsConfiguration, dnsView, gatewayHost, root
} }
} }
if !isValidDNSDeployType(cfg.DNSDeployType) {
return nil, errors.Errorf("%v is not a valid deployment type", cfg.DNSDeployType)
}
token, cookie, err := getBluecatGatewayToken(cfg) token, cookie, err := getBluecatGatewayToken(cfg)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get API token from Bluecat Gateway") 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.SkipTLSVerify) gatewayClient := NewGatewayClient(cookie, token, cfg.GatewayHost, cfg.DNSConfiguration, cfg.View, cfg.RootZone, cfg.DNSServerName, cfg.SkipTLSVerify)
provider := &BluecatProvider{ provider := &BluecatProvider{
domainFilter: domainFilter, domainFilter: domainFilter,
@ -183,6 +199,8 @@ func NewBluecatProvider(configFile, dnsConfiguration, dnsView, gatewayHost, root
dryRun: dryRun, dryRun: dryRun,
gatewayClient: gatewayClient, gatewayClient: gatewayClient,
DNSConfiguration: cfg.DNSConfiguration, DNSConfiguration: cfg.DNSConfiguration,
DNSServerName: cfg.DNSServerName,
DNSDeployType: cfg.DNSDeployType,
View: cfg.View, View: cfg.View,
RootZone: cfg.RootZone, RootZone: cfg.RootZone,
} }
@ -190,7 +208,7 @@ func NewBluecatProvider(configFile, dnsConfiguration, dnsView, gatewayHost, root
} }
// NewGatewayClient creates and returns a new Bluecat gateway client // NewGatewayClient creates and returns a new Bluecat gateway client
func NewGatewayClient(cookie http.Cookie, token, gatewayHost, dnsConfiguration, view, rootZone string, skipTLSVerify bool) GatewayClientConfig { func NewGatewayClient(cookie http.Cookie, token, gatewayHost, dnsConfiguration, view, rootZone, dnsServerName string, skipTLSVerify bool) GatewayClientConfig {
// TODO: do not handle defaulting here // 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 // Right now the Bluecat gateway doesn't seem to have a way to get the root zone from the API. If the user
@ -203,6 +221,7 @@ func NewGatewayClient(cookie http.Cookie, token, gatewayHost, dnsConfiguration,
Token: token, Token: token,
Host: gatewayHost, Host: gatewayHost,
DNSConfiguration: dnsConfiguration, DNSConfiguration: dnsConfiguration,
DNSServerName: dnsServerName,
View: view, View: view,
RootZone: rootZone, RootZone: rootZone,
SkipTLSVerify: skipTLSVerify, SkipTLSVerify: skipTLSVerify,
@ -292,8 +311,6 @@ func (p *BluecatProvider) Records(ctx context.Context) (endpoints []*endpoint.En
} }
endpoints = append(endpoints, ep) endpoints = append(endpoints, ep)
} }
// TODO: add bluecat deploy API call here
} }
log.Debugf("fetched %d records from Bluecat", len(endpoints)) log.Debugf("fetched %d records from Bluecat", len(endpoints))
@ -316,6 +333,20 @@ func (p *BluecatProvider) ApplyChanges(ctx context.Context, changes *plan.Change
p.deleteRecords(deleted) p.deleteRecords(deleted)
p.createRecords(created) p.createRecords(created)
if p.DNSServerName != "" {
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'")
}
} else {
log.Debug("Not executing deploy because server name was not provided")
}
return nil return nil
} }
@ -936,6 +967,41 @@ func (c GatewayClientConfig) deleteTXTRecord(name string, zone string) error {
return nil 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 //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) { func (c GatewayClientConfig) buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, url, body) req, err := http.NewRequest(method, url, body)
@ -961,6 +1027,17 @@ func splitProperties(props string) map[string]string {
return propMap 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, //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/' //such as '/zones/com/zones/example/zones/'
func expandZone(zone string) string { func expandZone(zone string) string {

View File

@ -109,6 +109,9 @@ func (g mockGatewayClient) deleteTXTRecord(name string, zone string) error {
*g.mockBluecatTXTs = nil *g.mockBluecatTXTs = nil
return nil return nil
} }
func (g mockGatewayClient) serverFullDeploy() error {
return nil
}
func (g mockGatewayClient) buildHTTPRequest(method, url string, body io.Reader) (*http.Request, error) { 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) request, _ := http.NewRequest("GET", fmt.Sprintf("%s/users", "http://some.com/api/v1"), nil)
@ -374,11 +377,12 @@ func TestBluecatNewGatewayClient(t *testing.T) {
testToken := "exampleToken" testToken := "exampleToken"
testgateWayHost := "exampleHost" testgateWayHost := "exampleHost"
testDNSConfiguration := "exampleDNSConfiguration" testDNSConfiguration := "exampleDNSConfiguration"
testDNSServer := "exampleServer"
testView := "testView" testView := "testView"
testZone := "example.com" testZone := "example.com"
testVerify := true testVerify := true
client := NewGatewayClient(testCookie, testToken, testgateWayHost, testDNSConfiguration, testView, testZone, testVerify) 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 { 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") t.Fatal("Client values dont match")
@ -475,6 +479,21 @@ func TestBluecatRecordset(t *testing.T) {
assert.Equal(t, cnameActual.res, cnameExpected.res) 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) { 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) assert.True(t, testutils.SameEndpoints(actual, expected), "actual and expected endpoints don't match. %s:%s", actual, expected)
} }