Rick Henry aab63ec4f7
Update references to UKFast to ANS group
Signed-off-by: Rick Henry <rick.henry@assureddigitaltech.com>
2022-06-23 22:02:49 +01:00

243 lines
7.2 KiB
Go

/*
Copyright 2021 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 safedns
import (
"context"
"fmt"
"os"
ansClient "github.com/ans-group/sdk-go/pkg/client"
ansConnection "github.com/ans-group/sdk-go/pkg/connection"
"github.com/ans-group/sdk-go/pkg/service/safedns"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
// SafeDNS is an interface that is a subset of the SafeDNS service API that are actually used.
// Signatures must match exactly.
type SafeDNS interface {
CreateZoneRecord(zoneName string, req safedns.CreateRecordRequest) (int, error)
DeleteZoneRecord(zoneName string, recordID int) error
GetZone(zoneName string) (safedns.Zone, error)
GetZoneRecord(zoneName string, recordID int) (safedns.Record, error)
GetZoneRecords(zoneName string, parameters ansConnection.APIRequestParameters) ([]safedns.Record, error)
GetZones(parameters ansConnection.APIRequestParameters) ([]safedns.Zone, error)
PatchZoneRecord(zoneName string, recordID int, patch safedns.PatchRecordRequest) (int, error)
UpdateZoneRecord(zoneName string, record safedns.Record) (int, error)
}
// SafeDNSProvider implements the DNS provider spec for UKFast SafeDNS.
type SafeDNSProvider struct {
provider.BaseProvider
Client SafeDNS
// Only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter
DryRun bool
APIRequestParams ansConnection.APIRequestParameters
}
// ZoneRecord is a datatype to simplify management of a record in a zone.
type ZoneRecord struct {
ID int
Name string
Type safedns.RecordType
TTL safedns.RecordTTL
Zone string
Content string
}
func NewSafeDNSProvider(domainFilter endpoint.DomainFilter, dryRun bool) (*SafeDNSProvider, error) {
token, ok := os.LookupEnv("SAFEDNS_TOKEN")
if !ok {
return nil, fmt.Errorf("no SAFEDNS_TOKEN found in environment")
}
ukfAPIConnection := ansConnection.NewAPIKeyCredentialsAPIConnection(token)
ansClient := ansClient.NewClient(ukfAPIConnection)
safeDNS := ansClient.SafeDNSService()
provider := &SafeDNSProvider{
Client: safeDNS,
domainFilter: domainFilter,
DryRun: dryRun,
APIRequestParams: *ansConnection.NewAPIRequestParameters(),
}
return provider, nil
}
// Zones returns the list of hosted zones in the SafeDNS account
func (p *SafeDNSProvider) Zones(ctx context.Context) ([]safedns.Zone, error) {
var zones []safedns.Zone
allZones, err := p.Client.GetZones(p.APIRequestParams)
if err != nil {
return nil, err
}
// Check each found zone to see whether they match the domain filter provided. If they do, append it to the array of
// zones defined above. If not, continue to the next item in the loop.
for _, zone := range allZones {
if p.domainFilter.Match(zone.Name) {
zones = append(zones, zone)
} else {
continue
}
}
return zones, nil
}
func (p *SafeDNSProvider) ZoneRecords(ctx context.Context) ([]ZoneRecord, error) {
zones, err := p.Zones(ctx)
if err != nil {
return nil, err
}
var zoneRecords []ZoneRecord
for _, zone := range zones {
// For each zone in the zonelist, get all records of an ExternalDNS supported type.
records, err := p.Client.GetZoneRecords(zone.Name, p.APIRequestParams)
if err != nil {
return nil, err
}
for _, r := range records {
zoneRecord := ZoneRecord{
ID: r.ID,
Name: r.Name,
Type: r.Type,
TTL: r.TTL,
Zone: zone.Name,
Content: r.Content,
}
zoneRecords = append(zoneRecords, zoneRecord)
}
}
return zoneRecords, nil
}
// Records returns a list of Endpoint resources created from all records in supported zones.
func (p *SafeDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint
zoneRecords, err := p.ZoneRecords(ctx)
if err != nil {
return nil, err
}
for _, r := range zoneRecords {
if provider.SupportedRecordType(string(r.Type)) {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, string(r.Type), endpoint.TTL(r.TTL), r.Content))
}
}
return endpoints, nil
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *SafeDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
// Identify the zone name for each record
zoneNameIDMapper := provider.ZoneIDName{}
zones, err := p.Zones(ctx)
if err != nil {
return err
}
for _, zone := range zones {
zoneNameIDMapper.Add(zone.Name, zone.Name)
}
zoneRecords, err := p.ZoneRecords(ctx)
if err != nil {
return err
}
for _, endpoint := range changes.Create {
_, ZoneName := zoneNameIDMapper.FindZone(endpoint.DNSName)
for _, target := range endpoint.Targets {
request := safedns.CreateRecordRequest{
Name: endpoint.DNSName,
Type: endpoint.RecordType,
Content: target,
}
log.WithFields(log.Fields{
"zoneID": ZoneName,
"dnsName": endpoint.DNSName,
"recordType": endpoint.RecordType,
"Value": target,
}).Info("Creating record")
_, err := p.Client.CreateZoneRecord(ZoneName, request)
if err != nil {
return err
}
}
}
for _, endpoint := range changes.UpdateNew {
// Currently iterates over each zoneRecord in ZoneRecords for each Endpoint
// in UpdateNew; the same will go for Delete. As it's double-iteration,
// that's O(n^2), which isn't great. No performance issues have been noted
// thus far.
var zoneRecord ZoneRecord
for _, target := range endpoint.Targets {
for _, zr := range zoneRecords {
if zr.Name == endpoint.DNSName && zr.Content == target {
zoneRecord = zr
break
}
}
newTTL := safedns.RecordTTL(int(endpoint.RecordTTL))
newRecord := safedns.PatchRecordRequest{
Name: endpoint.DNSName,
Content: target,
TTL: &newTTL,
Type: endpoint.RecordType,
}
log.WithFields(log.Fields{
"zoneID": zoneRecord.Zone,
"dnsName": newRecord.Name,
"recordType": newRecord.Type,
"Value": newRecord.Content,
"Priority": newRecord.Priority,
}).Info("Patching record")
_, err = p.Client.PatchZoneRecord(zoneRecord.Zone, zoneRecord.ID, newRecord)
if err != nil {
return err
}
}
}
for _, endpoint := range changes.Delete {
// As above, currently iterates in O(n^2). May be a good start for optimisations.
var zoneRecord ZoneRecord
for _, zr := range zoneRecords {
if zr.Name == endpoint.DNSName && string(zr.Type) == endpoint.RecordType {
zoneRecord = zr
break
}
}
log.WithFields(log.Fields{
"zoneID": zoneRecord.Zone,
"dnsName": zoneRecord.Name,
"recordType": zoneRecord.Type,
}).Info("Deleting record")
err := p.Client.DeleteZoneRecord(zoneRecord.Zone, zoneRecord.ID)
if err != nil {
return err
}
}
return nil
}