sigs.k8s.io/external-dns@v0.14.1/provider/dnsimple/dnsimple.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 dnsimple 18 19 import ( 20 "context" 21 "fmt" 22 "os" 23 "strconv" 24 "strings" 25 26 "github.com/dnsimple/dnsimple-go/dnsimple" 27 log "github.com/sirupsen/logrus" 28 "golang.org/x/oauth2" 29 30 "sigs.k8s.io/external-dns/endpoint" 31 "sigs.k8s.io/external-dns/pkg/apis/externaldns" 32 "sigs.k8s.io/external-dns/plan" 33 "sigs.k8s.io/external-dns/provider" 34 ) 35 36 const dnsimpleRecordTTL = 3600 // Default TTL of 1 hour if not set (DNSimple's default) 37 38 type dnsimpleIdentityService struct { 39 service *dnsimple.IdentityService 40 } 41 42 func (i dnsimpleIdentityService) Whoami(ctx context.Context) (*dnsimple.WhoamiResponse, error) { 43 return i.service.Whoami(ctx) 44 } 45 46 // dnsimpleZoneServiceInterface is an interface that contains all necessary zone services from DNSimple 47 type dnsimpleZoneServiceInterface interface { 48 ListZones(ctx context.Context, accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) 49 ListRecords(ctx context.Context, accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) 50 CreateRecord(ctx context.Context, accountID string, zoneID string, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) 51 DeleteRecord(ctx context.Context, accountID string, zoneID string, recordID int64) (*dnsimple.ZoneRecordResponse, error) 52 UpdateRecord(ctx context.Context, accountID string, zoneID string, recordID int64, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) 53 } 54 55 type dnsimpleZoneService struct { 56 service *dnsimple.ZonesService 57 } 58 59 func (z dnsimpleZoneService) ListZones(ctx context.Context, accountID string, options *dnsimple.ZoneListOptions) (*dnsimple.ZonesResponse, error) { 60 return z.service.ListZones(ctx, accountID, options) 61 } 62 63 func (z dnsimpleZoneService) ListRecords(ctx context.Context, accountID string, zoneID string, options *dnsimple.ZoneRecordListOptions) (*dnsimple.ZoneRecordsResponse, error) { 64 return z.service.ListRecords(ctx, accountID, zoneID, options) 65 } 66 67 func (z dnsimpleZoneService) CreateRecord(ctx context.Context, accountID string, zoneID string, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) { 68 return z.service.CreateRecord(ctx, accountID, zoneID, recordAttributes) 69 } 70 71 func (z dnsimpleZoneService) DeleteRecord(ctx context.Context, accountID string, zoneID string, recordID int64) (*dnsimple.ZoneRecordResponse, error) { 72 return z.service.DeleteRecord(ctx, accountID, zoneID, recordID) 73 } 74 75 func (z dnsimpleZoneService) UpdateRecord(ctx context.Context, accountID string, zoneID string, recordID int64, recordAttributes dnsimple.ZoneRecordAttributes) (*dnsimple.ZoneRecordResponse, error) { 76 return z.service.UpdateRecord(ctx, accountID, zoneID, recordID, recordAttributes) 77 } 78 79 type dnsimpleProvider struct { 80 provider.BaseProvider 81 client dnsimpleZoneServiceInterface 82 identity dnsimpleIdentityService 83 accountID string 84 domainFilter endpoint.DomainFilter 85 zoneIDFilter provider.ZoneIDFilter 86 dryRun bool 87 } 88 89 type dnsimpleChange struct { 90 Action string 91 ResourceRecordSet dnsimple.ZoneRecord 92 } 93 94 const ( 95 dnsimpleCreate = "CREATE" 96 dnsimpleDelete = "DELETE" 97 dnsimpleUpdate = "UPDATE" 98 ) 99 100 // NewDnsimpleProvider initializes a new Dnsimple based provider 101 func NewDnsimpleProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, dryRun bool) (provider.Provider, error) { 102 oauthToken := os.Getenv("DNSIMPLE_OAUTH") 103 if len(oauthToken) == 0 { 104 return nil, fmt.Errorf("no dnsimple oauth token provided") 105 } 106 107 ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: oauthToken}) 108 tc := oauth2.NewClient(context.Background(), ts) 109 110 client := dnsimple.NewClient(tc) 111 client.SetUserAgent(fmt.Sprintf("Kubernetes ExternalDNS/%s", externaldns.Version)) 112 113 provider := &dnsimpleProvider{ 114 client: dnsimpleZoneService{service: client.Zones}, 115 identity: dnsimpleIdentityService{service: client.Identity}, 116 domainFilter: domainFilter, 117 zoneIDFilter: zoneIDFilter, 118 dryRun: dryRun, 119 } 120 121 whoamiResponse, err := provider.identity.Whoami(context.Background()) 122 if err != nil { 123 return nil, err 124 } 125 provider.accountID = int64ToString(whoamiResponse.Data.Account.ID) 126 return provider, nil 127 } 128 129 // GetAccountID returns the account ID given DNSimple credentials. 130 func (p *dnsimpleProvider) GetAccountID(ctx context.Context) (accountID string, err error) { 131 // get DNSimple client accountID 132 whoamiResponse, err := p.identity.Whoami(ctx) 133 if err != nil { 134 return "", err 135 } 136 return int64ToString(whoamiResponse.Data.Account.ID), nil 137 } 138 139 // Returns a list of filtered Zones 140 func (p *dnsimpleProvider) Zones(ctx context.Context) (map[string]dnsimple.Zone, error) { 141 zones := make(map[string]dnsimple.Zone) 142 page := 1 143 listOptions := &dnsimple.ZoneListOptions{} 144 for { 145 listOptions.Page = &page 146 zonesResponse, err := p.client.ListZones(ctx, p.accountID, listOptions) 147 if err != nil { 148 return nil, err 149 } 150 for _, zone := range zonesResponse.Data { 151 if !p.domainFilter.Match(zone.Name) { 152 continue 153 } 154 155 if !p.zoneIDFilter.Match(int64ToString(zone.ID)) { 156 continue 157 } 158 159 zones[int64ToString(zone.ID)] = zone 160 } 161 162 page++ 163 if page > zonesResponse.Pagination.TotalPages { 164 break 165 } 166 } 167 return zones, nil 168 } 169 170 // Records returns a list of endpoints in a given zone 171 func (p *dnsimpleProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { 172 zones, err := p.Zones(ctx) 173 if err != nil { 174 return nil, err 175 } 176 for _, zone := range zones { 177 page := 1 178 listOptions := &dnsimple.ZoneRecordListOptions{} 179 for { 180 listOptions.Page = &page 181 records, err := p.client.ListRecords(ctx, p.accountID, zone.Name, listOptions) 182 if err != nil { 183 return nil, err 184 } 185 for _, record := range records.Data { 186 switch record.Type { 187 case "A", "CNAME", "TXT": 188 break 189 default: 190 continue 191 } 192 // Apex records have an empty string for their name. 193 // Consider this when creating the endpoint dnsName 194 dnsName := fmt.Sprintf("%s.%s", record.Name, record.ZoneID) 195 if record.Name == "" { 196 dnsName = record.ZoneID 197 } 198 endpoints = append(endpoints, endpoint.NewEndpointWithTTL(dnsName, record.Type, endpoint.TTL(record.TTL), record.Content)) 199 } 200 page++ 201 if page > records.Pagination.TotalPages { 202 break 203 } 204 } 205 } 206 return endpoints, nil 207 } 208 209 // newDnsimpleChange initializes a new change to dns records 210 func newDnsimpleChange(action string, e *endpoint.Endpoint) *dnsimpleChange { 211 ttl := dnsimpleRecordTTL 212 if e.RecordTTL.IsConfigured() { 213 ttl = int(e.RecordTTL) 214 } 215 216 change := &dnsimpleChange{ 217 Action: action, 218 ResourceRecordSet: dnsimple.ZoneRecord{ 219 Name: e.DNSName, 220 Type: e.RecordType, 221 Content: e.Targets[0], 222 TTL: ttl, 223 }, 224 } 225 return change 226 } 227 228 // newDnsimpleChanges returns a slice of changes based on given action and record 229 func newDnsimpleChanges(action string, endpoints []*endpoint.Endpoint) []*dnsimpleChange { 230 changes := make([]*dnsimpleChange, 0, len(endpoints)) 231 for _, e := range endpoints { 232 changes = append(changes, newDnsimpleChange(action, e)) 233 } 234 return changes 235 } 236 237 // submitChanges takes a zone and a collection of changes and makes all changes from the collection 238 func (p *dnsimpleProvider) submitChanges(ctx context.Context, changes []*dnsimpleChange) error { 239 if len(changes) == 0 { 240 log.Infof("All records are already up to date") 241 return nil 242 } 243 zones, err := p.Zones(ctx) 244 if err != nil { 245 return err 246 } 247 for _, change := range changes { 248 zone := dnsimpleSuitableZone(change.ResourceRecordSet.Name, zones) 249 if zone == nil { 250 log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", change.ResourceRecordSet.Name) 251 continue 252 } 253 254 log.Infof("Changing records: %s %v in zone: %s", change.Action, change.ResourceRecordSet, zone.Name) 255 256 if change.ResourceRecordSet.Name == zone.Name { 257 change.ResourceRecordSet.Name = "" // Apex records have an empty name 258 } else { 259 change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, fmt.Sprintf(".%s", zone.Name)) 260 } 261 262 recordAttributes := dnsimple.ZoneRecordAttributes{ 263 Name: &change.ResourceRecordSet.Name, 264 Type: change.ResourceRecordSet.Type, 265 Content: change.ResourceRecordSet.Content, 266 TTL: change.ResourceRecordSet.TTL, 267 } 268 269 if !p.dryRun { 270 switch change.Action { 271 case dnsimpleCreate: 272 _, err := p.client.CreateRecord(ctx, p.accountID, zone.Name, recordAttributes) 273 if err != nil { 274 return err 275 } 276 case dnsimpleDelete: 277 recordID, err := p.GetRecordID(ctx, zone.Name, *recordAttributes.Name) 278 if err != nil { 279 return err 280 } 281 _, err = p.client.DeleteRecord(ctx, p.accountID, zone.Name, recordID) 282 if err != nil { 283 return err 284 } 285 case dnsimpleUpdate: 286 recordID, err := p.GetRecordID(ctx, zone.Name, *recordAttributes.Name) 287 if err != nil { 288 return err 289 } 290 _, err = p.client.UpdateRecord(ctx, p.accountID, zone.Name, recordID, recordAttributes) 291 if err != nil { 292 return err 293 } 294 } 295 } 296 } 297 return nil 298 } 299 300 // GetRecordID returns the record ID for a given record name and zone. 301 func (p *dnsimpleProvider) GetRecordID(ctx context.Context, zone string, recordName string) (recordID int64, err error) { 302 page := 1 303 listOptions := &dnsimple.ZoneRecordListOptions{Name: &recordName} 304 for { 305 listOptions.Page = &page 306 records, err := p.client.ListRecords(ctx, p.accountID, zone, listOptions) 307 if err != nil { 308 return 0, err 309 } 310 311 for _, record := range records.Data { 312 if record.Name == recordName { 313 return record.ID, nil 314 } 315 } 316 317 page++ 318 if page > records.Pagination.TotalPages { 319 break 320 } 321 } 322 return 0, fmt.Errorf("no record id found") 323 } 324 325 // dnsimpleSuitableZone returns the most suitable zone for a given hostname and a set of zones. 326 func dnsimpleSuitableZone(hostname string, zones map[string]dnsimple.Zone) *dnsimple.Zone { 327 var zone *dnsimple.Zone 328 for _, z := range zones { 329 if strings.HasSuffix(hostname, z.Name) { 330 if zone == nil || len(z.Name) > len(zone.Name) { 331 newZ := z 332 zone = &newZ 333 } 334 } 335 } 336 return zone 337 } 338 339 // ApplyChanges applies a given set of changes 340 func (p *dnsimpleProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 341 combinedChanges := make([]*dnsimpleChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) 342 343 combinedChanges = append(combinedChanges, newDnsimpleChanges(dnsimpleCreate, changes.Create)...) 344 combinedChanges = append(combinedChanges, newDnsimpleChanges(dnsimpleUpdate, changes.UpdateNew)...) 345 combinedChanges = append(combinedChanges, newDnsimpleChanges(dnsimpleDelete, changes.Delete)...) 346 347 return p.submitChanges(ctx, combinedChanges) 348 } 349 350 func int64ToString(i int64) string { 351 return strconv.FormatInt(i, 10) 352 }