/* 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" ) var ( sbPoolActOnProbes = true ultradnsPoolType = "rdpool" accountName string sbPoolRunProbes = true // Setting custom headers for ultradns api calls 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") } return &UltraDNSProvider{ client: *client, domainFilter: domainFilter, dryRun: dryRun, }, 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 var 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 { switch change.ResourceRecordSetUltraDNS.RRType { case "CNAME": cnameownerName = change.ResourceRecordSetUltraDNS.OwnerName case "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 }