sigs.k8s.io/external-dns@v0.14.1/provider/vultr/vultr.go (about) 1 /* 2 Copyright 2020 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 vultr 18 19 import ( 20 "context" 21 "fmt" 22 "os" 23 "strings" 24 25 log "github.com/sirupsen/logrus" 26 "github.com/vultr/govultr/v2" 27 "golang.org/x/oauth2" 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 const ( 35 vultrCreate = "CREATE" 36 vultrDelete = "DELETE" 37 vultrUpdate = "UPDATE" 38 vultrTTL = 3600 39 ) 40 41 // VultrProvider is an implementation of Provider for Vultr DNS. 42 type VultrProvider struct { 43 provider.BaseProvider 44 client govultr.Client 45 46 domainFilter endpoint.DomainFilter 47 DryRun bool 48 } 49 50 // VultrChanges differentiates between ChangActions. 51 type VultrChanges struct { 52 Action string 53 54 ResourceRecordSet *govultr.DomainRecordReq 55 } 56 57 // NewVultrProvider initializes a new Vultr BNS based provider 58 func NewVultrProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*VultrProvider, error) { 59 apiKey, ok := os.LookupEnv("VULTR_API_KEY") 60 if !ok { 61 return nil, fmt.Errorf("no token found") 62 } 63 64 oauthClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{ 65 AccessToken: apiKey, 66 })) 67 client := govultr.NewClient(oauthClient) 68 client.SetUserAgent(fmt.Sprintf("ExternalDNS/%s", client.UserAgent)) 69 70 p := &VultrProvider{ 71 client: *client, 72 domainFilter: domainFilter, 73 DryRun: dryRun, 74 } 75 76 return p, nil 77 } 78 79 // Zones returns list of hosted zones 80 func (p *VultrProvider) Zones(ctx context.Context) ([]govultr.Domain, error) { 81 zones, err := p.fetchZones(ctx) 82 if err != nil { 83 return nil, err 84 } 85 86 return zones, nil 87 } 88 89 // Records returns the list of records. 90 func (p *VultrProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { 91 zones, err := p.Zones(ctx) 92 if err != nil { 93 return nil, err 94 } 95 96 var endpoints []*endpoint.Endpoint 97 98 for _, zone := range zones { 99 records, err := p.fetchRecords(ctx, zone.Domain) 100 if err != nil { 101 return nil, err 102 } 103 104 for _, r := range records { 105 if provider.SupportedRecordType(r.Type) { 106 name := fmt.Sprintf("%s.%s", r.Name, zone.Domain) 107 108 // root name is identified by the empty string and should be 109 // translated to zone name for the endpoint entry. 110 if r.Name == "" { 111 name = zone.Domain 112 } 113 114 endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, r.Type, endpoint.TTL(r.TTL), r.Data)) 115 } 116 } 117 } 118 119 return endpoints, nil 120 } 121 122 func (p *VultrProvider) fetchRecords(ctx context.Context, domain string) ([]govultr.DomainRecord, error) { 123 var allRecords []govultr.DomainRecord 124 listOptions := &govultr.ListOptions{} 125 126 for { 127 records, meta, err := p.client.DomainRecord.List(ctx, domain, listOptions) 128 if err != nil { 129 return nil, err 130 } 131 132 allRecords = append(allRecords, records...) 133 134 if meta.Links.Next == "" { 135 break 136 } else { 137 listOptions.Cursor = meta.Links.Next 138 continue 139 } 140 } 141 142 return allRecords, nil 143 } 144 145 func (p *VultrProvider) fetchZones(ctx context.Context) ([]govultr.Domain, error) { 146 var zones []govultr.Domain 147 listOptions := &govultr.ListOptions{} 148 149 for { 150 allZones, meta, err := p.client.Domain.List(ctx, listOptions) 151 if err != nil { 152 return nil, err 153 } 154 155 for _, zone := range allZones { 156 if p.domainFilter.Match(zone.Domain) { 157 zones = append(zones, zone) 158 } 159 } 160 161 if meta.Links.Next == "" { 162 break 163 } else { 164 listOptions.Cursor = meta.Links.Next 165 continue 166 } 167 } 168 169 return zones, nil 170 } 171 172 func (p *VultrProvider) submitChanges(ctx context.Context, changes []*VultrChanges) error { 173 if len(changes) == 0 { 174 log.Infof("All records are already up to date") 175 return nil 176 } 177 178 zones, err := p.Zones(ctx) 179 if err != nil { 180 return err 181 } 182 183 zoneChanges := separateChangesByZone(zones, changes) 184 185 for zoneName, changes := range zoneChanges { 186 for _, change := range changes { 187 log.WithFields(log.Fields{ 188 "record": change.ResourceRecordSet.Name, 189 "type": change.ResourceRecordSet.Type, 190 "ttl": change.ResourceRecordSet.TTL, 191 "action": change.Action, 192 "zone": zoneName, 193 }).Info("Changing record.") 194 195 switch change.Action { 196 case vultrCreate: 197 if _, err := p.client.DomainRecord.Create(ctx, zoneName, change.ResourceRecordSet); err != nil { 198 return err 199 } 200 case vultrDelete: 201 id, err := p.getRecordID(ctx, zoneName, change.ResourceRecordSet) 202 if err != nil { 203 return err 204 } 205 206 if err := p.client.DomainRecord.Delete(ctx, zoneName, id); err != nil { 207 return err 208 } 209 case vultrUpdate: 210 id, err := p.getRecordID(ctx, zoneName, change.ResourceRecordSet) 211 if err != nil { 212 return err 213 } 214 if err := p.client.DomainRecord.Update(ctx, zoneName, id, change.ResourceRecordSet); err != nil { 215 return err 216 } 217 } 218 } 219 } 220 return nil 221 } 222 223 // ApplyChanges applies a given set of changes in a given zone. 224 func (p *VultrProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 225 combinedChanges := make([]*VultrChanges, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) 226 227 combinedChanges = append(combinedChanges, newVultrChanges(vultrCreate, changes.Create)...) 228 combinedChanges = append(combinedChanges, newVultrChanges(vultrUpdate, changes.UpdateNew)...) 229 combinedChanges = append(combinedChanges, newVultrChanges(vultrDelete, changes.Delete)...) 230 231 return p.submitChanges(ctx, combinedChanges) 232 } 233 234 func newVultrChanges(action string, endpoints []*endpoint.Endpoint) []*VultrChanges { 235 changes := make([]*VultrChanges, 0, len(endpoints)) 236 ttl := vultrTTL 237 for _, e := range endpoints { 238 if e.RecordTTL.IsConfigured() { 239 ttl = int(e.RecordTTL) 240 } 241 242 change := &VultrChanges{ 243 Action: action, 244 ResourceRecordSet: &govultr.DomainRecordReq{ 245 Type: e.RecordType, 246 Name: e.DNSName, 247 Data: e.Targets[0], 248 TTL: ttl, 249 }, 250 } 251 252 changes = append(changes, change) 253 } 254 return changes 255 } 256 257 func separateChangesByZone(zones []govultr.Domain, changes []*VultrChanges) map[string][]*VultrChanges { 258 change := make(map[string][]*VultrChanges) 259 zoneNameID := provider.ZoneIDName{} 260 261 for _, z := range zones { 262 zoneNameID.Add(z.Domain, z.Domain) 263 change[z.Domain] = []*VultrChanges{} 264 } 265 266 for _, c := range changes { 267 zone, _ := zoneNameID.FindZone(c.ResourceRecordSet.Name) 268 if zone == "" { 269 log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.ResourceRecordSet.Name) 270 continue 271 } 272 change[zone] = append(change[zone], c) 273 } 274 return change 275 } 276 277 func (p *VultrProvider) getRecordID(ctx context.Context, zone string, record *govultr.DomainRecordReq) (recordID string, err error) { 278 listOptions := &govultr.ListOptions{} 279 for { 280 records, meta, err := p.client.DomainRecord.List(ctx, zone, listOptions) 281 if err != nil { 282 return "0", err 283 } 284 285 for _, r := range records { 286 strippedName := strings.TrimSuffix(record.Name, "."+zone) 287 if record.Name == zone { 288 strippedName = "" 289 } 290 291 if r.Name == strippedName && r.Type == record.Type { 292 return r.ID, nil 293 } 294 } 295 if meta.Links.Next == "" { 296 break 297 } else { 298 listOptions.Cursor = meta.Links.Next 299 continue 300 } 301 } 302 303 return "", fmt.Errorf("no record was found") 304 }