sigs.k8s.io/external-dns@v0.14.1/provider/vinyldns/vinyldns.go (about) 1 /* 2 Copyright 2017 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 vinyldns 18 19 import ( 20 "context" 21 "fmt" 22 "os" 23 "strings" 24 25 log "github.com/sirupsen/logrus" 26 "github.com/vinyldns/go-vinyldns/vinyldns" 27 28 "sigs.k8s.io/external-dns/endpoint" 29 "sigs.k8s.io/external-dns/plan" 30 "sigs.k8s.io/external-dns/provider" 31 ) 32 33 const ( 34 vinyldnsCreate = "CREATE" 35 vinyldnsDelete = "DELETE" 36 vinyldnsUpdate = "UPDATE" 37 38 vinyldnsRecordTTL = 300 39 ) 40 41 type vinyldnsZoneInterface interface { 42 Zones() ([]vinyldns.Zone, error) 43 RecordSets(id string) ([]vinyldns.RecordSet, error) 44 RecordSet(zoneID, recordSetID string) (vinyldns.RecordSet, error) 45 RecordSetCreate(rs *vinyldns.RecordSet) (*vinyldns.RecordSetUpdateResponse, error) 46 RecordSetUpdate(rs *vinyldns.RecordSet) (*vinyldns.RecordSetUpdateResponse, error) 47 RecordSetDelete(zoneID, recordSetID string) (*vinyldns.RecordSetUpdateResponse, error) 48 } 49 50 type vinyldnsProvider struct { 51 provider.BaseProvider 52 client vinyldnsZoneInterface 53 zoneFilter provider.ZoneIDFilter 54 domainFilter endpoint.DomainFilter 55 dryRun bool 56 } 57 58 type vinyldnsChange struct { 59 Action string 60 ResourceRecordSet vinyldns.RecordSet 61 } 62 63 // NewVinylDNSProvider provides support for VinylDNS records 64 func NewVinylDNSProvider(domainFilter endpoint.DomainFilter, zoneFilter provider.ZoneIDFilter, dryRun bool) (provider.Provider, error) { 65 _, ok := os.LookupEnv("VINYLDNS_ACCESS_KEY") 66 if !ok { 67 return nil, fmt.Errorf("no vinyldns access key found") 68 } 69 70 client := vinyldns.NewClientFromEnv() 71 72 return &vinyldnsProvider{ 73 client: client, 74 dryRun: dryRun, 75 zoneFilter: zoneFilter, 76 domainFilter: domainFilter, 77 }, nil 78 } 79 80 func (p *vinyldnsProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { 81 zones, err := p.client.Zones() 82 if err != nil { 83 return nil, err 84 } 85 86 for _, zone := range zones { 87 if !p.zoneFilter.Match(zone.ID) { 88 continue 89 } 90 91 if !p.domainFilter.Match(zone.Name) { 92 continue 93 } 94 95 log.Infof(fmt.Sprintf("Zone: [%s:%s]", zone.ID, zone.Name)) 96 records, err := p.client.RecordSets(zone.ID) 97 if err != nil { 98 return nil, err 99 } 100 101 for _, r := range records { 102 if provider.SupportedRecordType(r.Type) { 103 recordsCount := len(r.Records) 104 log.Debugf(fmt.Sprintf("%s.%s.%d.%s", r.Name, r.Type, recordsCount, zone.Name)) 105 106 // TODO: AAAA Records 107 if len(r.Records) > 0 { 108 targets := make([]string, len(r.Records)) 109 for idx, rr := range r.Records { 110 switch r.Type { 111 case "A": 112 targets[idx] = rr.Address 113 case "CNAME": 114 targets[idx] = rr.CName 115 case "TXT": 116 targets[idx] = rr.Text 117 } 118 } 119 120 endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name+"."+zone.Name, r.Type, endpoint.TTL(r.TTL), targets...)) 121 } 122 } 123 } 124 } 125 126 return endpoints, nil 127 } 128 129 func vinyldnsSuitableZone(hostname string, zones []vinyldns.Zone) *vinyldns.Zone { 130 var zone *vinyldns.Zone 131 for _, z := range zones { 132 log.Debugf("hostname: %s and zoneName: %s", hostname, z.Name) 133 // Adding a . as vinyl appends it to each zone record 134 if strings.HasSuffix(hostname+".", z.Name) { 135 zone = &z 136 break 137 } 138 } 139 return zone 140 } 141 142 func (p *vinyldnsProvider) submitChanges(changes []*vinyldnsChange) error { 143 if len(changes) == 0 { 144 log.Infof("All records are already up to date") 145 return nil 146 } 147 148 zones, err := p.client.Zones() 149 if err != nil { 150 return err 151 } 152 153 for _, change := range changes { 154 zone := vinyldnsSuitableZone(change.ResourceRecordSet.Name, zones) 155 if zone == nil { 156 log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", change.ResourceRecordSet.Name) 157 continue 158 } 159 160 change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name+".", "."+zone.Name) 161 change.ResourceRecordSet.ZoneID = zone.ID 162 log.Infof("Changing records: %s %v in zone: %s", change.Action, change.ResourceRecordSet, zone.Name) 163 164 if !p.dryRun { 165 switch change.Action { 166 case vinyldnsCreate: 167 _, err := p.client.RecordSetCreate(&change.ResourceRecordSet) 168 if err != nil { 169 return err 170 } 171 case vinyldnsUpdate: 172 recordID, err := p.findRecordSetID(zone.ID, change.ResourceRecordSet.Name) 173 if err != nil { 174 return err 175 } 176 change.ResourceRecordSet.ID = recordID 177 _, err = p.client.RecordSetUpdate(&change.ResourceRecordSet) 178 if err != nil { 179 return err 180 } 181 case vinyldnsDelete: 182 recordID, err := p.findRecordSetID(zone.ID, change.ResourceRecordSet.Name) 183 if err != nil { 184 return err 185 } 186 _, err = p.client.RecordSetDelete(zone.ID, recordID) 187 if err != nil { 188 return err 189 } 190 } 191 } 192 } 193 194 return nil 195 } 196 197 func (p *vinyldnsProvider) findRecordSetID(zoneID string, recordSetName string) (recordID string, err error) { 198 records, err := p.client.RecordSets(zoneID) 199 if err != nil { 200 return "", err 201 } 202 203 for _, r := range records { 204 if r.Name == recordSetName { 205 return r.ID, nil 206 } 207 } 208 209 return "", fmt.Errorf("record not found") 210 } 211 212 func (p *vinyldnsProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 213 combinedChanges := make([]*vinyldnsChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) 214 215 combinedChanges = append(combinedChanges, newVinylDNSChanges(vinyldnsCreate, changes.Create)...) 216 combinedChanges = append(combinedChanges, newVinylDNSChanges(vinyldnsUpdate, changes.UpdateNew)...) 217 combinedChanges = append(combinedChanges, newVinylDNSChanges(vinyldnsDelete, changes.Delete)...) 218 219 return p.submitChanges(combinedChanges) 220 } 221 222 // newVinylDNSChanges returns a collection of Changes based on the given records and action. 223 func newVinylDNSChanges(action string, endpoints []*endpoint.Endpoint) []*vinyldnsChange { 224 changes := make([]*vinyldnsChange, 0, len(endpoints)) 225 226 for _, e := range endpoints { 227 changes = append(changes, newVinylDNSChange(action, e)) 228 } 229 230 return changes 231 } 232 233 func newVinylDNSChange(action string, endpoint *endpoint.Endpoint) *vinyldnsChange { 234 ttl := vinyldnsRecordTTL 235 if endpoint.RecordTTL.IsConfigured() { 236 ttl = int(endpoint.RecordTTL) 237 } 238 239 records := []vinyldns.Record{} 240 241 // TODO: AAAA 242 if endpoint.RecordType == "CNAME" { 243 records = []vinyldns.Record{ 244 { 245 CName: endpoint.Targets[0], 246 }, 247 } 248 } else if endpoint.RecordType == "TXT" { 249 records = []vinyldns.Record{ 250 { 251 Text: endpoint.Targets[0], 252 }, 253 } 254 } else if endpoint.RecordType == "A" { 255 records = []vinyldns.Record{ 256 { 257 Address: endpoint.Targets[0], 258 }, 259 } 260 } 261 262 change := &vinyldnsChange{ 263 Action: action, 264 ResourceRecordSet: vinyldns.RecordSet{ 265 Name: endpoint.DNSName, 266 Type: endpoint.RecordType, 267 TTL: ttl, 268 Records: records, 269 }, 270 } 271 return change 272 }