sigs.k8s.io/external-dns@v0.14.1/provider/bluecat/gateway/api.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 // TODO: add logging 17 // TODO: add timeouts 18 package api 19 20 import ( 21 "bytes" 22 "crypto/tls" 23 "encoding/json" 24 "io" 25 "net/http" 26 "os" 27 "strings" 28 29 "github.com/pkg/errors" 30 log "github.com/sirupsen/logrus" 31 ) 32 33 // TODO: Ensure DNS Deploy Type Defaults to no-deploy instead of "" 34 type BluecatConfig struct { 35 GatewayHost string `json:"gatewayHost"` 36 GatewayUsername string `json:"gatewayUsername,omitempty"` 37 GatewayPassword string `json:"gatewayPassword,omitempty"` 38 DNSConfiguration string `json:"dnsConfiguration"` 39 DNSServerName string `json:"dnsServerName"` 40 DNSDeployType string `json:"dnsDeployType"` 41 View string `json:"dnsView"` 42 RootZone string `json:"rootZone"` 43 SkipTLSVerify bool `json:"skipTLSVerify"` 44 } 45 46 type GatewayClient interface { 47 GetBluecatZones(zoneName string) ([]BluecatZone, error) 48 GetHostRecords(zone string, records *[]BluecatHostRecord) error 49 GetCNAMERecords(zone string, records *[]BluecatCNAMERecord) error 50 GetHostRecord(name string, record *BluecatHostRecord) error 51 GetCNAMERecord(name string, record *BluecatCNAMERecord) error 52 CreateHostRecord(zone string, req *BluecatCreateHostRecordRequest) error 53 CreateCNAMERecord(zone string, req *BluecatCreateCNAMERecordRequest) error 54 DeleteHostRecord(name string, zone string) (err error) 55 DeleteCNAMERecord(name string, zone string) (err error) 56 GetTXTRecords(zone string, records *[]BluecatTXTRecord) error 57 GetTXTRecord(name string, record *BluecatTXTRecord) error 58 CreateTXTRecord(zone string, req *BluecatCreateTXTRecordRequest) error 59 DeleteTXTRecord(name string, zone string) error 60 ServerFullDeploy() error 61 } 62 63 // GatewayClientConfig defines the configuration for a Bluecat Gateway Client 64 type GatewayClientConfig struct { 65 Cookie http.Cookie 66 Token string 67 Host string 68 DNSConfiguration string 69 View string 70 RootZone string 71 DNSServerName string 72 SkipTLSVerify bool 73 } 74 75 // BluecatZone defines a zone to hold records 76 type BluecatZone struct { 77 ID int `json:"id"` 78 Name string `json:"name"` 79 Properties string `json:"properties"` 80 Type string `json:"type"` 81 } 82 83 // BluecatHostRecord defines dns Host record 84 type BluecatHostRecord struct { 85 ID int `json:"id"` 86 Name string `json:"name"` 87 Properties string `json:"properties"` 88 Type string `json:"type"` 89 } 90 91 // BluecatCNAMERecord defines dns CNAME record 92 type BluecatCNAMERecord struct { 93 ID int `json:"id"` 94 Name string `json:"name"` 95 Properties string `json:"properties"` 96 Type string `json:"type"` 97 } 98 99 // BluecatTXTRecord defines dns TXT record 100 type BluecatTXTRecord struct { 101 ID int `json:"id"` 102 Name string `json:"name"` 103 Properties string `json:"properties"` 104 } 105 106 type BluecatCreateHostRecordRequest struct { 107 AbsoluteName string `json:"absolute_name"` 108 IP4Address string `json:"ip4_address"` 109 TTL int `json:"ttl"` 110 Properties string `json:"properties"` 111 } 112 113 type BluecatCreateCNAMERecordRequest struct { 114 AbsoluteName string `json:"absolute_name"` 115 LinkedRecord string `json:"linked_record"` 116 TTL int `json:"ttl"` 117 Properties string `json:"properties"` 118 } 119 120 type BluecatCreateTXTRecordRequest struct { 121 AbsoluteName string `json:"absolute_name"` 122 Text string `json:"txt"` 123 } 124 125 type BluecatServerFullDeployRequest struct { 126 ServerName string `json:"server_name"` 127 } 128 129 // NewGatewayClient creates and returns a new Bluecat gateway client 130 func NewGatewayClientConfig(cookie http.Cookie, token, gatewayHost, dnsConfiguration, view, rootZone, dnsServerName string, skipTLSVerify bool) GatewayClientConfig { 131 // TODO: do not handle defaulting here 132 // 133 // Right now the Bluecat gateway doesn't seem to have a way to get the root zone from the API. If the user 134 // doesn't provide one via the config file we'll assume it's 'com' 135 if rootZone == "" { 136 rootZone = "com" 137 } 138 return GatewayClientConfig{ 139 Cookie: cookie, 140 Token: token, 141 Host: gatewayHost, 142 DNSConfiguration: dnsConfiguration, 143 DNSServerName: dnsServerName, 144 View: view, 145 RootZone: rootZone, 146 SkipTLSVerify: skipTLSVerify, 147 } 148 } 149 150 // GetBluecatGatewayToken retrieves a Bluecat Gateway API token. 151 func GetBluecatGatewayToken(cfg BluecatConfig) (string, http.Cookie, error) { 152 var username string 153 if cfg.GatewayUsername != "" { 154 username = cfg.GatewayUsername 155 } 156 if v, ok := os.LookupEnv("BLUECAT_USERNAME"); ok { 157 username = v 158 } 159 160 var password string 161 if cfg.GatewayPassword != "" { 162 password = cfg.GatewayPassword 163 } 164 if v, ok := os.LookupEnv("BLUECAT_PASSWORD"); ok { 165 password = v 166 } 167 168 body, err := json.Marshal(map[string]string{ 169 "username": username, 170 "password": password, 171 }) 172 if err != nil { 173 return "", http.Cookie{}, errors.Wrap(err, "could not unmarshal credentials for bluecat gateway config") 174 } 175 url := cfg.GatewayHost + "/rest_login" 176 177 response, err := executeHTTPRequest(cfg.SkipTLSVerify, http.MethodPost, url, "", bytes.NewBuffer(body), http.Cookie{}) 178 if err != nil { 179 return "", http.Cookie{}, errors.Wrap(err, "error obtaining API token from bluecat gateway") 180 } 181 defer response.Body.Close() 182 183 responseBody, err := io.ReadAll(response.Body) 184 if err != nil { 185 return "", http.Cookie{}, errors.Wrap(err, "failed to read login response from bluecat gateway") 186 } 187 188 if response.StatusCode != http.StatusOK { 189 return "", http.Cookie{}, errors.Errorf("got HTTP response code %v, detailed message: %v", response.StatusCode, string(responseBody)) 190 } 191 192 jsonResponse := map[string]string{} 193 err = json.Unmarshal(responseBody, &jsonResponse) 194 if err != nil { 195 return "", http.Cookie{}, errors.Wrap(err, "error unmarshaling json response (auth) from bluecat gateway") 196 } 197 198 // Example response: {"access_token": "BAMAuthToken: abc123"} 199 // We only care about the actual token string - i.e. abc123 200 // The gateway also creates a cookie as part of the response. This seems to be the actual auth mechanism, at least 201 // for now. 202 return strings.Split(jsonResponse["access_token"], " ")[1], *response.Cookies()[0], nil 203 } 204 205 func (c GatewayClientConfig) GetBluecatZones(zoneName string) ([]BluecatZone, error) { 206 zonePath := expandZone(zoneName) 207 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath 208 209 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) 210 if err != nil { 211 return nil, errors.Wrapf(err, "error requesting zones from gateway: %v, %v", url, zoneName) 212 } 213 defer response.Body.Close() 214 215 if response.StatusCode != http.StatusOK { 216 return nil, errors.Errorf("received http %v requesting zones from gateway in zone %v", response.StatusCode, zoneName) 217 } 218 219 zones := []BluecatZone{} 220 json.NewDecoder(response.Body).Decode(&zones) 221 222 // Bluecat Gateway only returns subzones one level deeper than the provided zone 223 // so this recursion is needed to traverse subzones until none are returned 224 for _, zone := range zones { 225 zoneProps := SplitProperties(zone.Properties) 226 subZones, err := c.GetBluecatZones(zoneProps["absoluteName"]) 227 if err != nil { 228 return nil, errors.Wrapf(err, "error retrieving subzones from gateway: %v", zoneName) 229 } 230 zones = append(zones, subZones...) 231 } 232 233 return zones, nil 234 } 235 236 func (c GatewayClientConfig) GetHostRecords(zone string, records *[]BluecatHostRecord) error { 237 zonePath := expandZone(zone) 238 // Remove the trailing 'zones/' 239 zonePath = strings.TrimSuffix(zonePath, "zones/") 240 241 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/" 242 243 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) 244 if err != nil { 245 return errors.Wrapf(err, "error requesting host records from gateway in zone %v", zone) 246 } 247 defer response.Body.Close() 248 249 if response.StatusCode != http.StatusOK { 250 return errors.Errorf("received http %v requesting host records from gateway in zone %v", response.StatusCode, zone) 251 } 252 253 json.NewDecoder(response.Body).Decode(records) 254 log.Debugf("Get Host Records Response: %v", records) 255 256 return nil 257 } 258 259 func (c GatewayClientConfig) GetCNAMERecords(zone string, records *[]BluecatCNAMERecord) error { 260 zonePath := expandZone(zone) 261 // Remove the trailing 'zones/' 262 zonePath = strings.TrimSuffix(zonePath, "zones/") 263 264 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/" 265 266 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) 267 if err != nil { 268 return errors.Wrapf(err, "error retrieving cname records from gateway in zone %v", zone) 269 } 270 defer response.Body.Close() 271 272 if response.StatusCode != http.StatusOK { 273 return errors.Errorf("received http %v requesting cname records from gateway in zone %v", response.StatusCode, zone) 274 } 275 276 json.NewDecoder(response.Body).Decode(records) 277 log.Debugf("Get CName Records Response: %v", records) 278 279 return nil 280 } 281 282 func (c GatewayClientConfig) GetTXTRecords(zone string, records *[]BluecatTXTRecord) error { 283 zonePath := expandZone(zone) 284 // Remove the trailing 'zones/' 285 zonePath = strings.TrimSuffix(zonePath, "zones/") 286 287 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/" 288 289 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) 290 if err != nil { 291 return errors.Wrapf(err, "error retrieving txt records from gateway in zone %v", zone) 292 } 293 defer response.Body.Close() 294 295 if response.StatusCode != http.StatusOK { 296 return errors.Errorf("received http %v requesting txt records from gateway in zone %v", response.StatusCode, zone) 297 } 298 299 log.Debugf("Get Txt Records response: %v", response) 300 json.NewDecoder(response.Body).Decode(records) 301 log.Debugf("Get TXT Records Body: %v", records) 302 303 return nil 304 } 305 306 func (c GatewayClientConfig) GetHostRecord(name string, record *BluecatHostRecord) error { 307 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + 308 "/views/" + c.View + "/" + 309 "host_records/" + name + "/" 310 311 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) 312 if err != nil { 313 return errors.Wrapf(err, "error retrieving host record %v from gateway", name) 314 } 315 defer response.Body.Close() 316 317 if response.StatusCode != http.StatusOK { 318 return errors.Errorf("received http %v while retrieving host record %v from gateway", response.StatusCode, name) 319 } 320 321 json.NewDecoder(response.Body).Decode(record) 322 log.Debugf("Get Host Record Response: %v", record) 323 return nil 324 } 325 326 func (c GatewayClientConfig) GetCNAMERecord(name string, record *BluecatCNAMERecord) error { 327 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + 328 "/views/" + c.View + "/" + 329 "cname_records/" + name + "/" 330 331 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) 332 if err != nil { 333 return errors.Wrapf(err, "error retrieving cname record %v from gateway", name) 334 } 335 defer response.Body.Close() 336 337 if response.StatusCode != http.StatusOK { 338 return errors.Errorf("received http %v while retrieving cname record %v from gateway", response.StatusCode, name) 339 } 340 341 json.NewDecoder(response.Body).Decode(record) 342 log.Debugf("Get CName Record Response: %v", record) 343 return nil 344 } 345 346 func (c GatewayClientConfig) GetTXTRecord(name string, record *BluecatTXTRecord) error { 347 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + 348 "/views/" + c.View + "/" + 349 "text_records/" + name + "/" 350 351 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodGet, url, c.Token, nil, c.Cookie) 352 if err != nil { 353 return errors.Wrapf(err, "error retrieving record %v from gateway", name) 354 } 355 defer response.Body.Close() 356 357 if response.StatusCode != http.StatusOK { 358 return errors.Errorf("received http %v while retrieving txt record %v from gateway", response.StatusCode, name) 359 } 360 361 json.NewDecoder(response.Body).Decode(record) 362 log.Debugf("Get TXT Record Response: %v", record) 363 364 return nil 365 } 366 367 func (c GatewayClientConfig) CreateHostRecord(zone string, req *BluecatCreateHostRecordRequest) error { 368 zonePath := expandZone(zone) 369 // Remove the trailing 'zones/' 370 zonePath = strings.TrimSuffix(zonePath, "zones/") 371 372 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "host_records/" 373 body, err := json.Marshal(req) 374 if err != nil { 375 return errors.Wrap(err, "could not marshal body for create host record") 376 } 377 378 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie) 379 if err != nil { 380 return errors.Wrapf(err, "error creating host record %v in gateway", req.AbsoluteName) 381 } 382 defer response.Body.Close() 383 384 if response.StatusCode != http.StatusCreated { 385 return errors.Errorf("received http %v while creating host record %v in gateway", response.StatusCode, req.AbsoluteName) 386 } 387 388 return nil 389 } 390 391 func (c GatewayClientConfig) CreateCNAMERecord(zone string, req *BluecatCreateCNAMERecordRequest) error { 392 zonePath := expandZone(zone) 393 // Remove the trailing 'zones/' 394 zonePath = strings.TrimSuffix(zonePath, "zones/") 395 396 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "cname_records/" 397 body, err := json.Marshal(req) 398 if err != nil { 399 return errors.Wrap(err, "could not marshal body for create cname record") 400 } 401 402 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie) 403 if err != nil { 404 return errors.Wrapf(err, "error creating cname record %v in gateway", req.AbsoluteName) 405 } 406 defer response.Body.Close() 407 408 if response.StatusCode != http.StatusCreated { 409 return errors.Errorf("received http %v while creating cname record %v to alias %v in gateway", response.StatusCode, req.AbsoluteName, req.LinkedRecord) 410 } 411 412 return nil 413 } 414 415 func (c GatewayClientConfig) CreateTXTRecord(zone string, req *BluecatCreateTXTRecordRequest) error { 416 zonePath := expandZone(zone) 417 // Remove the trailing 'zones/' 418 zonePath = strings.TrimSuffix(zonePath, "zones/") 419 420 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath + "text_records/" 421 body, err := json.Marshal(req) 422 if err != nil { 423 return errors.Wrap(err, "could not marshal body for create txt record") 424 } 425 426 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie) 427 if err != nil { 428 return errors.Wrapf(err, "error creating txt record %v in gateway", req.AbsoluteName) 429 } 430 defer response.Body.Close() 431 432 if response.StatusCode != http.StatusCreated { 433 return errors.Errorf("received http %v while creating txt record %v in gateway", response.StatusCode, req.AbsoluteName) 434 } 435 436 return nil 437 } 438 439 func (c GatewayClientConfig) DeleteHostRecord(name string, zone string) (err error) { 440 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + 441 "/views/" + c.View + "/" + 442 "host_records/" + name + "." + zone + "/" 443 444 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodDelete, url, c.Token, nil, c.Cookie) 445 if err != nil { 446 return errors.Wrapf(err, "error deleting host record %v from gateway", name) 447 } 448 449 if response.StatusCode != http.StatusNoContent { 450 return errors.Errorf("received http %v while deleting host record %v from gateway", response.StatusCode, name) 451 } 452 453 return nil 454 } 455 456 func (c GatewayClientConfig) DeleteCNAMERecord(name string, zone string) (err error) { 457 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + 458 "/views/" + c.View + "/" + 459 "cname_records/" + name + "." + zone + "/" 460 461 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodDelete, url, c.Token, nil, c.Cookie) 462 if err != nil { 463 return errors.Wrapf(err, "error deleting cname record %v from gateway", name) 464 } 465 if response.StatusCode != http.StatusNoContent { 466 return errors.Errorf("received http %v while deleting cname record %v from gateway", response.StatusCode, name) 467 } 468 469 return nil 470 } 471 472 func (c GatewayClientConfig) DeleteTXTRecord(name string, zone string) error { 473 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + 474 "/views/" + c.View + "/" + 475 "text_records/" + name + "." + zone + "/" 476 477 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodDelete, url, c.Token, nil, c.Cookie) 478 if err != nil { 479 return errors.Wrapf(err, "error deleting txt record %v from gateway", name) 480 } 481 if response.StatusCode != http.StatusNoContent { 482 return errors.Errorf("received http %v while deleting txt record %v from gateway", response.StatusCode, name) 483 } 484 485 return nil 486 } 487 488 func (c GatewayClientConfig) ServerFullDeploy() error { 489 log.Infof("Executing full deploy on server %s", c.DNSServerName) 490 url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/server/full_deploy/" 491 requestBody := BluecatServerFullDeployRequest{ 492 ServerName: c.DNSServerName, 493 } 494 495 body, err := json.Marshal(requestBody) 496 if err != nil { 497 return errors.Wrap(err, "could not marshal body for server full deploy") 498 } 499 500 response, err := executeHTTPRequest(c.SkipTLSVerify, http.MethodPost, url, c.Token, bytes.NewBuffer(body), c.Cookie) 501 if err != nil { 502 return errors.Wrap(err, "error executing full deploy") 503 } 504 505 if response.StatusCode != http.StatusCreated { 506 responseBody, err := io.ReadAll(response.Body) 507 if err != nil { 508 return errors.Wrap(err, "failed to read full deploy response body") 509 } 510 return errors.Errorf("got HTTP response code %v, detailed message: %v", response.StatusCode, string(responseBody)) 511 } 512 513 return nil 514 } 515 516 // SplitProperties is a helper function to break a '|' separated string into key/value pairs 517 // i.e. "foo=bar|baz=mop" 518 func SplitProperties(props string) map[string]string { 519 propMap := make(map[string]string) 520 // remove trailing | character before we split 521 props = strings.TrimSuffix(props, "|") 522 523 splits := strings.Split(props, "|") 524 for _, pair := range splits { 525 items := strings.Split(pair, "=") 526 propMap[items[0]] = items[1] 527 } 528 529 return propMap 530 } 531 532 // IsValidDNSDeployType validates the deployment type provided by a users configuration is supported by the Bluecat Provider. 533 func IsValidDNSDeployType(deployType string) bool { 534 validDNSDeployTypes := []string{"no-deploy", "full-deploy"} 535 for _, t := range validDNSDeployTypes { 536 if t == deployType { 537 return true 538 } 539 } 540 return false 541 } 542 543 // expandZone takes an absolute domain name such as 'example.com' and returns a zone hierarchy used by Bluecat Gateway, 544 // such as '/zones/com/zones/example/zones/' 545 func expandZone(zone string) string { 546 ze := "zones/" 547 parts := strings.Split(zone, ".") 548 if len(parts) > 1 { 549 last := len(parts) - 1 550 for i := range parts { 551 ze = ze + parts[last-i] + "/zones/" 552 } 553 } else { 554 ze = ze + zone + "/zones/" 555 } 556 return ze 557 } 558 559 func executeHTTPRequest(skipTLSVerify bool, method, url, token string, body io.Reader, cookie http.Cookie) (*http.Response, error) { 560 httpClient := &http.Client{ 561 Transport: &http.Transport{ 562 Proxy: http.ProxyFromEnvironment, 563 TLSClientConfig: &tls.Config{ 564 InsecureSkipVerify: skipTLSVerify, 565 }, 566 }, 567 } 568 request, err := http.NewRequest(method, url, body) 569 if err != nil { 570 return nil, err 571 } 572 if request.Method == http.MethodPost { 573 request.Header.Add("Content-Type", "application/json") 574 } 575 request.Header.Add("Accept", "application/json") 576 577 if token != "" { 578 request.Header.Add("Authorization", "Basic "+token) 579 } 580 request.AddCookie(&cookie) 581 582 return httpClient.Do(request) 583 }