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 |
| dnsView | Yes |
| rootZone | Yes |
| dnsServerName | No |
| dnsDeployType | No |
| skipTLSVerify | No (default false) |
#### Deploy

View File

@ -212,7 +212,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.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":
p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "vultr":

View File

@ -96,6 +96,8 @@ type Config struct {
BluecatDNSView string
BluecatGatewayHost string
BluecatRootZone string
BluecatDNSServerName string
BluecatDNSDeployType string
BluecatSkipTLSVerify bool
CloudflareProxied bool
CloudflareZonesPerPage int
@ -228,6 +230,7 @@ var defaultConfig = &Config{
AzureResourceGroup: "",
AzureSubscriptionID: "",
BluecatConfigFile: "/etc/kubernetes/bluecat.json",
BluecatDNSDeployType: "no-deploy",
CloudflareProxied: false,
CloudflareZonesPerPage: 50,
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-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-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-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: "",
AzureSubscriptionID: "",
BluecatDNSConfiguration: "",
BluecatDNSServerName: "",
BluecatConfigFile: "/etc/kubernetes/bluecat.json",
BluecatDNSView: "",
BluecatGatewayHost: "",
BluecatRootZone: "",
BluecatDNSDeployType: defaultConfig.BluecatDNSDeployType,
BluecatSkipTLSVerify: false,
CloudflareProxied: false,
CloudflareZonesPerPage: 50,
@ -162,10 +164,12 @@ var (
AzureResourceGroup: "arg",
AzureSubscriptionID: "arg",
BluecatDNSConfiguration: "arg",
BluecatDNSServerName: "arg",
BluecatConfigFile: "bluecat.json",
BluecatDNSView: "arg",
BluecatGatewayHost: "arg",
BluecatRootZone: "arg",
BluecatDNSDeployType: "full-deploy",
BluecatSkipTLSVerify: true,
CloudflareProxied: true,
CloudflareZonesPerPage: 20,
@ -270,8 +274,10 @@ func TestParseFlags(t *testing.T) {
"--bluecat-dns-configuration=arg",
"--bluecat-config-file=bluecat.json",
"--bluecat-dns-view=arg",
"--bluecat-dns-server-name=arg",
"--bluecat-gateway-host=arg",
"--bluecat-root-zone=arg",
"--bluecat-dns-deploy-type=full-deploy",
"--bluecat-skip-tls-verify",
"--cloudflare-proxied",
"--cloudflare-zones-per-page=20",
@ -379,6 +385,8 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg",
"EXTERNAL_DNS_AZURE_SUBSCRIPTION_ID": "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_DNS_VIEW": "arg",
"EXTERNAL_DNS_BLUECAT_GATEWAY_HOST": "arg",

View File

@ -44,6 +44,8 @@ type bluecatConfig struct {
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"`
@ -57,6 +59,8 @@ type BluecatProvider struct {
dryRun bool
RootZone string
DNSConfiguration string
DNSServerName string
DNSDeployType string
View string
gatewayClient GatewayClient
}
@ -76,6 +80,7 @@ type GatewayClient interface {
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
@ -86,6 +91,7 @@ type GatewayClientConfig struct {
DNSConfiguration string
View string
RootZone string
DNSServerName string
SkipTLSVerify bool
}
@ -144,10 +150,14 @@ type bluecatCreateTXTRecordRequest struct {
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, 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{}
contents, err := os.ReadFile(configFile)
if err != nil {
@ -155,6 +165,8 @@ func NewBluecatProvider(configFile, dnsConfiguration, dnsView, gatewayHost, root
cfg = bluecatConfig{
GatewayHost: gatewayHost,
DNSConfiguration: dnsConfiguration,
DNSServerName: dnsServerName,
DNSDeployType: dnsDeployType,
View: dnsView,
RootZone: rootZone,
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)
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.SkipTLSVerify)
gatewayClient := NewGatewayClient(cookie, token, cfg.GatewayHost, cfg.DNSConfiguration, cfg.View, cfg.RootZone, cfg.DNSServerName, cfg.SkipTLSVerify)
provider := &BluecatProvider{
domainFilter: domainFilter,
@ -183,6 +199,8 @@ func NewBluecatProvider(configFile, dnsConfiguration, dnsView, gatewayHost, root
dryRun: dryRun,
gatewayClient: gatewayClient,
DNSConfiguration: cfg.DNSConfiguration,
DNSServerName: cfg.DNSServerName,
DNSDeployType: cfg.DNSDeployType,
View: cfg.View,
RootZone: cfg.RootZone,
}
@ -190,7 +208,7 @@ func NewBluecatProvider(configFile, dnsConfiguration, dnsView, gatewayHost, root
}
// 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
//
// 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,
Host: gatewayHost,
DNSConfiguration: dnsConfiguration,
DNSServerName: dnsServerName,
View: view,
RootZone: rootZone,
SkipTLSVerify: skipTLSVerify,
@ -292,8 +311,6 @@ func (p *BluecatProvider) Records(ctx context.Context) (endpoints []*endpoint.En
}
endpoints = append(endpoints, ep)
}
// TODO: add bluecat deploy API call here
}
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.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
}
@ -936,6 +967,41 @@ func (c GatewayClientConfig) deleteTXTRecord(name string, zone string) error {
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)
@ -961,6 +1027,17 @@ func splitProperties(props string) map[string]string {
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 {

View File

@ -109,6 +109,9 @@ func (g mockGatewayClient) deleteTXTRecord(name string, zone string) error {
*g.mockBluecatTXTs = nil
return nil
}
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)
@ -374,11 +377,12 @@ func TestBluecatNewGatewayClient(t *testing.T) {
testToken := "exampleToken"
testgateWayHost := "exampleHost"
testDNSConfiguration := "exampleDNSConfiguration"
testDNSServer := "exampleServer"
testView := "testView"
testZone := "example.com"
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 {
t.Fatal("Client values dont match")
@ -475,6 +479,21 @@ 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)
}