From 8aef3e089f9759f4f4b73a4379b48c20caffd57c Mon Sep 17 00:00:00 2001 From: Vinny Sabatini Date: Fri, 4 Feb 2022 17:20:04 -0600 Subject: [PATCH] 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. --- docs/tutorials/bluecat.md | 2 + main.go | 2 +- pkg/apis/externaldns/types.go | 5 ++ pkg/apis/externaldns/types_test.go | 8 +++ provider/bluecat/bluecat.go | 87 ++++++++++++++++++++++++++++-- provider/bluecat/bluecat_test.go | 21 +++++++- 6 files changed, 118 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/bluecat.md b/docs/tutorials/bluecat.md index 0ce578717..0645d0454 100644 --- a/docs/tutorials/bluecat.md +++ b/docs/tutorials/bluecat.md @@ -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 diff --git a/main.go b/main.go index 3ef760edb..939d4c3fe 100644 --- a/main.go +++ b/main.go @@ -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": diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 0aa3eff77..917d12e51 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -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) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 4d6104493..e24208fcb 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -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", diff --git a/provider/bluecat/bluecat.go b/provider/bluecat/bluecat.go index 6d3434e1a..e54a84e65 100644 --- a/provider/bluecat/bluecat.go +++ b/provider/bluecat/bluecat.go @@ -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 { diff --git a/provider/bluecat/bluecat_test.go b/provider/bluecat/bluecat_test.go index 8793a95b8..17f541265 100644 --- a/provider/bluecat/bluecat_test.go +++ b/provider/bluecat/bluecat_test.go @@ -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) }