external-dns/provider/ultradns/ultradns.go
Andy Bursavich f6392be41e
gofumpt
2022-09-22 10:44:50 +01:00

503 lines
14 KiB
Go

/*
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 ultradns
import (
"context"
"encoding/base64"
"fmt"
"os"
"strconv"
"strings"
"time"
log "github.com/sirupsen/logrus"
udnssdk "github.com/ultradns/ultradns-sdk-go"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
const (
ultradnsCreate = "CREATE"
ultradnsDelete = "DELETE"
ultradnsUpdate = "UPDATE"
sbPoolPriority = 1
sbPoolOrder = "ROUND_ROBIN"
rdPoolOrder = "ROUND_ROBIN"
)
// global variables
var sbPoolRunProbes = true
var (
sbPoolActOnProbes = true
ultradnsPoolType = "rdpool"
accountName string
)
// Setting custom headers for ultradns api calls
var customHeader = []udnssdk.CustomHeader{
{
Key: "UltraClient",
Value: "kube-client",
},
}
// UltraDNSProvider struct
type UltraDNSProvider struct {
provider.BaseProvider
client udnssdk.Client
domainFilter endpoint.DomainFilter
dryRun bool
}
// UltraDNSChanges struct
type UltraDNSChanges struct {
Action string
ResourceRecordSetUltraDNS udnssdk.RRSet
}
// NewUltraDNSProvider initializes a new UltraDNS DNS based provider
func NewUltraDNSProvider(domainFilter endpoint.DomainFilter, dryRun bool) (*UltraDNSProvider, error) {
username, ok := os.LookupEnv("ULTRADNS_USERNAME")
udnssdk.SetCustomHeader = customHeader
if !ok {
return nil, fmt.Errorf("no username found")
}
base64password, ok := os.LookupEnv("ULTRADNS_PASSWORD")
if !ok {
return nil, fmt.Errorf("no password found")
}
// Base64 Standard Decoding
password, err := base64.StdEncoding.DecodeString(base64password)
if err != nil {
fmt.Printf("Error decoding string: %s ", err.Error())
return nil, err
}
baseURL, ok := os.LookupEnv("ULTRADNS_BASEURL")
if !ok {
return nil, fmt.Errorf("no baseurl found")
}
accountName, ok = os.LookupEnv("ULTRADNS_ACCOUNTNAME")
if !ok {
accountName = ""
}
probeValue, ok := os.LookupEnv("ULTRADNS_ENABLE_PROBING")
if ok {
if (probeValue != "true") && (probeValue != "false") {
return nil, fmt.Errorf("please set proper probe value, the values can be either true or false")
}
sbPoolRunProbes, _ = strconv.ParseBool(probeValue)
}
actOnProbeValue, ok := os.LookupEnv("ULTRADNS_ENABLE_ACTONPROBE")
if ok {
if (actOnProbeValue != "true") && (actOnProbeValue != "false") {
return nil, fmt.Errorf("please set proper act on probe value, the values can be either true or false")
}
sbPoolActOnProbes, _ = strconv.ParseBool(actOnProbeValue)
}
poolValue, ok := os.LookupEnv("ULTRADNS_POOL_TYPE")
if ok {
if (poolValue != "sbpool") && (poolValue != "rdpool") {
return nil, fmt.Errorf(" please set proper ULTRADNS_POOL_TYPE, supported types are sbpool or rdpool")
}
ultradnsPoolType = poolValue
}
client, err := udnssdk.NewClient(username, string(password), baseURL)
if err != nil {
return nil, fmt.Errorf("connection cannot be established")
}
provider := &UltraDNSProvider{
client: *client,
domainFilter: domainFilter,
dryRun: dryRun,
}
return provider, nil
}
// Zones returns list of hosted zones
func (p *UltraDNSProvider) Zones(ctx context.Context) ([]udnssdk.Zone, error) {
zoneKey := &udnssdk.ZoneKey{}
var err error
if p.domainFilter.IsConfigured() {
zonesAppender := []udnssdk.Zone{}
for _, zone := range p.domainFilter.Filters {
zoneKey.Zone = zone
zoneKey.AccountName = accountName
zones, err := p.fetchZones(ctx, zoneKey)
if err != nil {
return nil, err
}
zonesAppender = append(zonesAppender, zones...)
}
return zonesAppender, nil
}
zoneKey.AccountName = accountName
zones, err := p.fetchZones(ctx, zoneKey)
if err != nil {
return nil, err
}
return zones, nil
}
func (p *UltraDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint
zones, err := p.Zones(ctx)
if err != nil {
return nil, err
}
for _, zone := range zones {
log.Infof("zones : %v", zone)
var rrsetType string
var ownerName string
rrsetKey := udnssdk.RRSetKey{
Zone: zone.Properties.Name,
Type: rrsetType,
Name: ownerName,
}
if zone.Properties.ResourceRecordCount != 0 {
records, err := p.fetchRecords(ctx, rrsetKey)
if err != nil {
return nil, err
}
for _, r := range records {
recordTypeArray := strings.Fields(r.RRType)
if provider.SupportedRecordType(recordTypeArray[0]) {
log.Infof("owner name %s", r.OwnerName)
name := r.OwnerName
// root name is identified by the empty string and should be
// translated to zone name for the endpoint entry.
if r.OwnerName == "" {
name = zone.Properties.Name
}
endPointTTL := endpoint.NewEndpointWithTTL(name, recordTypeArray[0], endpoint.TTL(r.TTL), r.RData...)
endpoints = append(endpoints, endPointTTL)
}
}
}
}
log.Infof("endpoints %v", endpoints)
return endpoints, nil
}
func (p *UltraDNSProvider) fetchRecords(ctx context.Context, k udnssdk.RRSetKey) ([]udnssdk.RRSet, error) {
// Logic to paginate through all available results
maxerrs := 5
waittime := 5 * time.Second
rrsets := []udnssdk.RRSet{}
errcnt := 0
offset := 0
limit := 1000
for {
reqRrsets, ri, res, err := p.client.RRSets.SelectWithOffsetWithLimit(k, offset, limit)
if err != nil {
if res != nil && res.StatusCode >= 500 {
errcnt = errcnt + 1
if errcnt < maxerrs {
time.Sleep(waittime)
continue
}
}
return rrsets, err
}
rrsets = append(rrsets, reqRrsets...)
if ri.ReturnedCount+ri.Offset >= ri.TotalCount {
return rrsets, nil
}
offset = ri.ReturnedCount + ri.Offset
continue
}
}
func (p *UltraDNSProvider) fetchZones(ctx context.Context, zoneKey *udnssdk.ZoneKey) ([]udnssdk.Zone, error) {
// Logic to paginate through all available results
offset := 0
limit := 1000
maxerrs := 5
waittime := 5 * time.Second
zones := []udnssdk.Zone{}
errcnt := 0
for {
reqZones, ri, res, err := p.client.Zone.SelectWithOffsetWithLimit(zoneKey, offset, limit)
if err != nil {
if res != nil && res.StatusCode >= 500 {
errcnt = errcnt + 1
if errcnt < maxerrs {
time.Sleep(waittime)
continue
}
}
return zones, err
}
zones = append(zones, reqZones...)
if ri.ReturnedCount+ri.Offset >= ri.TotalCount {
return zones, nil
}
offset = ri.ReturnedCount + ri.Offset
continue
}
}
func (p *UltraDNSProvider) submitChanges(ctx context.Context, changes []*UltraDNSChanges) error {
cnameownerName := "cname"
txtownerName := "txt"
if len(changes) == 0 {
log.Infof("All records are already up to date")
return nil
}
zones, err := p.Zones(ctx)
if err != nil {
return err
}
zoneChanges := seperateChangeByZone(zones, changes)
for zoneName, changes := range zoneChanges {
for _, change := range changes {
if change.ResourceRecordSetUltraDNS.RRType == "CNAME" {
cnameownerName = change.ResourceRecordSetUltraDNS.OwnerName
} else if change.ResourceRecordSetUltraDNS.RRType == "TXT" {
txtownerName = change.ResourceRecordSetUltraDNS.OwnerName
}
if cnameownerName == txtownerName {
rrsetKey := udnssdk.RRSetKey{
Zone: zoneName,
Type: endpoint.RecordTypeCNAME,
Name: change.ResourceRecordSetUltraDNS.OwnerName,
}
err := p.getSpecificRecord(ctx, rrsetKey)
if err != nil {
return err
}
if !p.dryRun {
_, err = p.client.RRSets.Delete(rrsetKey)
if err != nil {
return err
}
}
return fmt.Errorf("the 'cname' and 'txt' record name cannot be same please recreate external-dns with - --txt-prefix=")
}
rrsetKey := udnssdk.RRSetKey{
Zone: zoneName,
Type: change.ResourceRecordSetUltraDNS.RRType,
Name: change.ResourceRecordSetUltraDNS.OwnerName,
}
record := udnssdk.RRSet{}
if (change.ResourceRecordSetUltraDNS.RRType == "A" || change.ResourceRecordSetUltraDNS.RRType == "AAAA") && (len(change.ResourceRecordSetUltraDNS.RData) >= 2) {
if ultradnsPoolType == "sbpool" && change.ResourceRecordSetUltraDNS.RRType == "A" {
sbPoolObject, _ := p.newSBPoolObjectCreation(ctx, change)
record = udnssdk.RRSet{
RRType: change.ResourceRecordSetUltraDNS.RRType,
OwnerName: change.ResourceRecordSetUltraDNS.OwnerName,
RData: change.ResourceRecordSetUltraDNS.RData,
TTL: change.ResourceRecordSetUltraDNS.TTL,
Profile: sbPoolObject.RawProfile(),
}
} else if ultradnsPoolType == "rdpool" {
rdPoolObject, _ := p.newRDPoolObjectCreation(ctx, change)
record = udnssdk.RRSet{
RRType: change.ResourceRecordSetUltraDNS.RRType,
OwnerName: change.ResourceRecordSetUltraDNS.OwnerName,
RData: change.ResourceRecordSetUltraDNS.RData,
TTL: change.ResourceRecordSetUltraDNS.TTL,
Profile: rdPoolObject.RawProfile(),
}
} else {
return fmt.Errorf("we do not support Multiple target 'aaaa' records in sb pool please contact to neustar for further details")
}
} else {
record = udnssdk.RRSet{
RRType: change.ResourceRecordSetUltraDNS.RRType,
OwnerName: change.ResourceRecordSetUltraDNS.OwnerName,
RData: change.ResourceRecordSetUltraDNS.RData,
TTL: change.ResourceRecordSetUltraDNS.TTL,
}
}
log.WithFields(log.Fields{
"record": record.OwnerName,
"type": record.RRType,
"ttl": record.TTL,
"action": change.Action,
"zone": zoneName,
"profile": record.Profile,
}).Info("Changing record.")
switch change.Action {
case ultradnsCreate:
if !p.dryRun {
res, err := p.client.RRSets.Create(rrsetKey, record)
_ = res
if err != nil {
return err
}
}
case ultradnsDelete:
err := p.getSpecificRecord(ctx, rrsetKey)
if err != nil {
return err
}
if !p.dryRun {
_, err = p.client.RRSets.Delete(rrsetKey)
if err != nil {
return err
}
}
case ultradnsUpdate:
err := p.getSpecificRecord(ctx, rrsetKey)
if err != nil {
return err
}
if !p.dryRun {
_, err = p.client.RRSets.Update(rrsetKey, record)
if err != nil {
return err
}
}
}
}
}
return nil
}
func (p *UltraDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
combinedChanges := make([]*UltraDNSChanges, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
log.Infof("value of changes %v,%v,%v", changes.Create, changes.UpdateNew, changes.Delete)
combinedChanges = append(combinedChanges, newUltraDNSChanges(ultradnsCreate, changes.Create)...)
combinedChanges = append(combinedChanges, newUltraDNSChanges(ultradnsUpdate, changes.UpdateNew)...)
combinedChanges = append(combinedChanges, newUltraDNSChanges(ultradnsDelete, changes.Delete)...)
return p.submitChanges(ctx, combinedChanges)
}
func newUltraDNSChanges(action string, endpoints []*endpoint.Endpoint) []*UltraDNSChanges {
changes := make([]*UltraDNSChanges, 0, len(endpoints))
var ttl int
for _, e := range endpoints {
if e.RecordTTL.IsConfigured() {
ttl = int(e.RecordTTL)
}
// Adding suffix dot to the record name
recordName := fmt.Sprintf("%s.", e.DNSName)
change := &UltraDNSChanges{
Action: action,
ResourceRecordSetUltraDNS: udnssdk.RRSet{
RRType: e.RecordType,
OwnerName: recordName,
RData: e.Targets,
TTL: ttl,
},
}
changes = append(changes, change)
}
return changes
}
func seperateChangeByZone(zones []udnssdk.Zone, changes []*UltraDNSChanges) map[string][]*UltraDNSChanges {
change := make(map[string][]*UltraDNSChanges)
zoneNameID := provider.ZoneIDName{}
for _, z := range zones {
zoneNameID.Add(z.Properties.Name, z.Properties.Name)
change[z.Properties.Name] = []*UltraDNSChanges{}
}
for _, c := range changes {
zone, _ := zoneNameID.FindZone(c.ResourceRecordSetUltraDNS.OwnerName)
if zone == "" {
log.Infof("Skipping record %s because no hosted zone matching record DNS Name was detected", c.ResourceRecordSetUltraDNS.OwnerName)
continue
}
change[zone] = append(change[zone], c)
}
return change
}
func (p *UltraDNSProvider) getSpecificRecord(ctx context.Context, rrsetKey udnssdk.RRSetKey) (err error) {
_, err = p.client.RRSets.Select(rrsetKey)
if err != nil {
return fmt.Errorf("no record was found for %v", rrsetKey)
}
return nil
}
// Creation of SBPoolObject
func (p *UltraDNSProvider) newSBPoolObjectCreation(ctx context.Context, change *UltraDNSChanges) (sbPool udnssdk.SBPoolProfile, err error) {
sbpoolRDataList := []udnssdk.SBRDataInfo{}
for range change.ResourceRecordSetUltraDNS.RData {
rrdataInfo := udnssdk.SBRDataInfo{
RunProbes: sbPoolRunProbes,
Priority: sbPoolPriority,
State: "NORMAL",
Threshold: 1,
Weight: nil,
}
sbpoolRDataList = append(sbpoolRDataList, rrdataInfo)
}
sbPoolObject := udnssdk.SBPoolProfile{
Context: udnssdk.SBPoolSchema,
Order: sbPoolOrder,
Description: change.ResourceRecordSetUltraDNS.OwnerName,
MaxActive: len(change.ResourceRecordSetUltraDNS.RData),
MaxServed: len(change.ResourceRecordSetUltraDNS.RData),
RDataInfo: sbpoolRDataList,
RunProbes: sbPoolRunProbes,
ActOnProbes: sbPoolActOnProbes,
}
return sbPoolObject, nil
}
// Creation of RDPoolObject
func (p *UltraDNSProvider) newRDPoolObjectCreation(ctx context.Context, change *UltraDNSChanges) (rdPool udnssdk.RDPoolProfile, err error) {
rdPoolObject := udnssdk.RDPoolProfile{
Context: udnssdk.RDPoolSchema,
Order: rdPoolOrder,
Description: change.ResourceRecordSetUltraDNS.OwnerName,
}
return rdPoolObject, nil
}