sigs.k8s.io/external-dns@v0.14.1/provider/safedns/safedns.go (about) 1 /* 2 Copyright 2021 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 17 package safedns 18 19 import ( 20 "context" 21 "fmt" 22 "os" 23 24 ansClient "github.com/ans-group/sdk-go/pkg/client" 25 ansConnection "github.com/ans-group/sdk-go/pkg/connection" 26 "github.com/ans-group/sdk-go/pkg/service/safedns" 27 log "github.com/sirupsen/logrus" 28 29 "sigs.k8s.io/external-dns/endpoint" 30 "sigs.k8s.io/external-dns/plan" 31 "sigs.k8s.io/external-dns/provider" 32 ) 33 34 // SafeDNS is an interface that is a subset of the SafeDNS service API that are actually used. 35 // Signatures must match exactly. 36 type SafeDNS interface { 37 CreateZoneRecord(zoneName string, req safedns.CreateRecordRequest) (int, error) 38 DeleteZoneRecord(zoneName string, recordID int) error 39 GetZone(zoneName string) (safedns.Zone, error) 40 GetZoneRecord(zoneName string, recordID int) (safedns.Record, error) 41 GetZoneRecords(zoneName string, parameters ansConnection.APIRequestParameters) ([]safedns.Record, error) 42 GetZones(parameters ansConnection.APIRequestParameters) ([]safedns.Zone, error) 43 PatchZoneRecord(zoneName string, recordID int, patch safedns.PatchRecordRequest) (int, error) 44 UpdateZoneRecord(zoneName string, record safedns.Record) (int, error) 45 } 46 47 // SafeDNSProvider implements the DNS provider spec for UKFast SafeDNS. 48 type SafeDNSProvider struct { 49 provider.BaseProvider 50 Client SafeDNS 51 // Only consider hosted zones managing domains ending in this suffix 52 domainFilter endpoint.DomainFilter 53 DryRun bool 54 APIRequestParams ansConnection.APIRequestParameters 55 } 56 57 // ZoneRecord is a datatype to simplify management of a record in a zone. 58 type ZoneRecord struct { 59 ID int 60 Name string 61 Type safedns.RecordType 62 TTL safedns.RecordTTL 63 Zone string 64 Content string 65 } 66 67 func NewSafeDNSProvider(domainFilter endpoint.DomainFilter, dryRun bool) (*SafeDNSProvider, error) { 68 token, ok := os.LookupEnv("SAFEDNS_TOKEN") 69 if !ok { 70 return nil, fmt.Errorf("no SAFEDNS_TOKEN found in environment") 71 } 72 73 ukfAPIConnection := ansConnection.NewAPIKeyCredentialsAPIConnection(token) 74 ansClient := ansClient.NewClient(ukfAPIConnection) 75 safeDNS := ansClient.SafeDNSService() 76 77 provider := &SafeDNSProvider{ 78 Client: safeDNS, 79 domainFilter: domainFilter, 80 DryRun: dryRun, 81 APIRequestParams: *ansConnection.NewAPIRequestParameters(), 82 } 83 return provider, nil 84 } 85 86 // Zones returns the list of hosted zones in the SafeDNS account 87 func (p *SafeDNSProvider) Zones(ctx context.Context) ([]safedns.Zone, error) { 88 var zones []safedns.Zone 89 90 allZones, err := p.Client.GetZones(p.APIRequestParams) 91 if err != nil { 92 return nil, err 93 } 94 95 // Check each found zone to see whether they match the domain filter provided. If they do, append it to the array of 96 // zones defined above. If not, continue to the next item in the loop. 97 for _, zone := range allZones { 98 if p.domainFilter.Match(zone.Name) { 99 zones = append(zones, zone) 100 } else { 101 continue 102 } 103 } 104 return zones, nil 105 } 106 107 func (p *SafeDNSProvider) ZoneRecords(ctx context.Context) ([]ZoneRecord, error) { 108 zones, err := p.Zones(ctx) 109 if err != nil { 110 return nil, err 111 } 112 113 var zoneRecords []ZoneRecord 114 for _, zone := range zones { 115 // For each zone in the zonelist, get all records of an ExternalDNS supported type. 116 records, err := p.Client.GetZoneRecords(zone.Name, p.APIRequestParams) 117 if err != nil { 118 return nil, err 119 } 120 for _, r := range records { 121 zoneRecord := ZoneRecord{ 122 ID: r.ID, 123 Name: r.Name, 124 Type: r.Type, 125 TTL: r.TTL, 126 Zone: zone.Name, 127 Content: r.Content, 128 } 129 zoneRecords = append(zoneRecords, zoneRecord) 130 } 131 } 132 return zoneRecords, nil 133 } 134 135 // Records returns a list of Endpoint resources created from all records in supported zones. 136 func (p *SafeDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { 137 var endpoints []*endpoint.Endpoint 138 zoneRecords, err := p.ZoneRecords(ctx) 139 if err != nil { 140 return nil, err 141 } 142 for _, r := range zoneRecords { 143 if provider.SupportedRecordType(string(r.Type)) { 144 endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, string(r.Type), endpoint.TTL(r.TTL), r.Content)) 145 } 146 } 147 return endpoints, nil 148 } 149 150 // ApplyChanges applies a given set of changes in a given zone. 151 func (p *SafeDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 152 // Identify the zone name for each record 153 zoneNameIDMapper := provider.ZoneIDName{} 154 155 zones, err := p.Zones(ctx) 156 if err != nil { 157 return err 158 } 159 for _, zone := range zones { 160 zoneNameIDMapper.Add(zone.Name, zone.Name) 161 } 162 163 zoneRecords, err := p.ZoneRecords(ctx) 164 if err != nil { 165 return err 166 } 167 168 for _, endpoint := range changes.Create { 169 _, ZoneName := zoneNameIDMapper.FindZone(endpoint.DNSName) 170 for _, target := range endpoint.Targets { 171 request := safedns.CreateRecordRequest{ 172 Name: endpoint.DNSName, 173 Type: endpoint.RecordType, 174 Content: target, 175 } 176 log.WithFields(log.Fields{ 177 "zoneID": ZoneName, 178 "dnsName": endpoint.DNSName, 179 "recordType": endpoint.RecordType, 180 "Value": target, 181 }).Info("Creating record") 182 _, err := p.Client.CreateZoneRecord(ZoneName, request) 183 if err != nil { 184 return err 185 } 186 } 187 } 188 for _, endpoint := range changes.UpdateNew { 189 // Currently iterates over each zoneRecord in ZoneRecords for each Endpoint 190 // in UpdateNew; the same will go for Delete. As it's double-iteration, 191 // that's O(n^2), which isn't great. No performance issues have been noted 192 // thus far. 193 var zoneRecord ZoneRecord 194 for _, target := range endpoint.Targets { 195 for _, zr := range zoneRecords { 196 if zr.Name == endpoint.DNSName && zr.Content == target { 197 zoneRecord = zr 198 break 199 } 200 } 201 202 newTTL := safedns.RecordTTL(int(endpoint.RecordTTL)) 203 newRecord := safedns.PatchRecordRequest{ 204 Name: endpoint.DNSName, 205 Content: target, 206 TTL: &newTTL, 207 Type: endpoint.RecordType, 208 } 209 log.WithFields(log.Fields{ 210 "zoneID": zoneRecord.Zone, 211 "dnsName": newRecord.Name, 212 "recordType": newRecord.Type, 213 "Value": newRecord.Content, 214 "Priority": newRecord.Priority, 215 }).Info("Patching record") 216 _, err = p.Client.PatchZoneRecord(zoneRecord.Zone, zoneRecord.ID, newRecord) 217 if err != nil { 218 return err 219 } 220 } 221 } 222 for _, endpoint := range changes.Delete { 223 // As above, currently iterates in O(n^2). May be a good start for optimisations. 224 var zoneRecord ZoneRecord 225 for _, zr := range zoneRecords { 226 if zr.Name == endpoint.DNSName && string(zr.Type) == endpoint.RecordType { 227 zoneRecord = zr 228 break 229 } 230 } 231 log.WithFields(log.Fields{ 232 "zoneID": zoneRecord.Zone, 233 "dnsName": zoneRecord.Name, 234 "recordType": zoneRecord.Type, 235 }).Info("Deleting record") 236 err := p.Client.DeleteZoneRecord(zoneRecord.Zone, zoneRecord.ID) 237 if err != nil { 238 return err 239 } 240 } 241 return nil 242 }