sigs.k8s.io/external-dns@v0.14.1/provider/exoscale/exoscale.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 exoscale 18 19 import ( 20 "context" 21 "strings" 22 23 egoscale "github.com/exoscale/egoscale/v2" 24 exoapi "github.com/exoscale/egoscale/v2/api" 25 log "github.com/sirupsen/logrus" 26 27 "sigs.k8s.io/external-dns/endpoint" 28 "sigs.k8s.io/external-dns/plan" 29 "sigs.k8s.io/external-dns/provider" 30 ) 31 32 // EgoscaleClientI for replaceable implementation 33 type EgoscaleClientI interface { 34 ListDNSDomainRecords(context.Context, string, string) ([]egoscale.DNSDomainRecord, error) 35 ListDNSDomains(context.Context, string) ([]egoscale.DNSDomain, error) 36 CreateDNSDomainRecord(context.Context, string, string, *egoscale.DNSDomainRecord) (*egoscale.DNSDomainRecord, error) 37 DeleteDNSDomainRecord(context.Context, string, string, *egoscale.DNSDomainRecord) error 38 UpdateDNSDomainRecord(context.Context, string, string, *egoscale.DNSDomainRecord) error 39 } 40 41 // ExoscaleProvider initialized as dns provider with no records 42 type ExoscaleProvider struct { 43 provider.BaseProvider 44 domain endpoint.DomainFilter 45 client EgoscaleClientI 46 apiEnv string 47 apiZone string 48 filter *zoneFilter 49 OnApplyChanges func(changes *plan.Changes) 50 dryRun bool 51 } 52 53 // ExoscaleOption for Provider options 54 type ExoscaleOption func(*ExoscaleProvider) 55 56 // NewExoscaleProvider returns ExoscaleProvider DNS provider interface implementation 57 func NewExoscaleProvider(env, zone, key, secret string, dryRun bool, opts ...ExoscaleOption) (*ExoscaleProvider, error) { 58 client, err := egoscale.NewClient( 59 key, 60 secret, 61 ) 62 if err != nil { 63 return nil, err 64 } 65 66 return NewExoscaleProviderWithClient(client, env, zone, dryRun, opts...), nil 67 } 68 69 // NewExoscaleProviderWithClient returns ExoscaleProvider DNS provider interface implementation (Client provided) 70 func NewExoscaleProviderWithClient(client EgoscaleClientI, env, zone string, dryRun bool, opts ...ExoscaleOption) *ExoscaleProvider { 71 ep := &ExoscaleProvider{ 72 filter: &zoneFilter{}, 73 OnApplyChanges: func(changes *plan.Changes) {}, 74 domain: endpoint.NewDomainFilter([]string{""}), 75 client: client, 76 apiEnv: env, 77 apiZone: zone, 78 dryRun: dryRun, 79 } 80 for _, opt := range opts { 81 opt(ep) 82 } 83 return ep 84 } 85 86 func (ep *ExoscaleProvider) getZones(ctx context.Context) (map[string]string, error) { 87 ctx = exoapi.WithEndpoint(ctx, exoapi.NewReqEndpoint(ep.apiEnv, ep.apiZone)) 88 domains, err := ep.client.ListDNSDomains(ctx, ep.apiZone) 89 if err != nil { 90 return nil, err 91 } 92 93 zones := map[string]string{} 94 for _, domain := range domains { 95 zones[*domain.ID] = *domain.UnicodeName 96 } 97 98 return zones, nil 99 } 100 101 // ApplyChanges simply modifies DNS via exoscale API 102 func (ep *ExoscaleProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 103 ep.OnApplyChanges(changes) 104 105 if ep.dryRun { 106 log.Infof("Will NOT delete these records: %+v", changes.Delete) 107 log.Infof("Will NOT create these records: %+v", changes.Create) 108 log.Infof("Will NOT update these records: %+v", merge(changes.UpdateOld, changes.UpdateNew)) 109 return nil 110 } 111 112 ctx = exoapi.WithEndpoint(ctx, exoapi.NewReqEndpoint(ep.apiEnv, ep.apiZone)) 113 114 zones, err := ep.getZones(ctx) 115 if err != nil { 116 return err 117 } 118 119 for _, epoint := range changes.Create { 120 if !ep.domain.Match(epoint.DNSName) { 121 continue 122 } 123 124 zoneID, name := ep.filter.EndpointZoneID(epoint, zones) 125 if zoneID == "" { 126 continue 127 } 128 129 // API does not accept 0 as default TTL but wants nil pointer instead 130 var ttl *int64 131 if epoint.RecordTTL != 0 { 132 t := int64(epoint.RecordTTL) 133 ttl = &t 134 } 135 record := egoscale.DNSDomainRecord{ 136 Name: &name, 137 Type: &epoint.RecordType, 138 TTL: ttl, 139 Content: &epoint.Targets[0], 140 } 141 _, err := ep.client.CreateDNSDomainRecord(ctx, ep.apiZone, zoneID, &record) 142 if err != nil { 143 return err 144 } 145 } 146 147 for _, epoint := range changes.UpdateNew { 148 if !ep.domain.Match(epoint.DNSName) { 149 continue 150 } 151 152 zoneID, name := ep.filter.EndpointZoneID(epoint, zones) 153 if zoneID == "" { 154 continue 155 } 156 157 records, err := ep.client.ListDNSDomainRecords(ctx, ep.apiZone, zoneID) 158 if err != nil { 159 return err 160 } 161 162 for _, record := range records { 163 if *record.Name != name { 164 continue 165 } 166 167 record.Type = &epoint.RecordType 168 record.Content = &epoint.Targets[0] 169 if epoint.RecordTTL != 0 { 170 ttl := int64(epoint.RecordTTL) 171 record.TTL = &ttl 172 } 173 174 err = ep.client.UpdateDNSDomainRecord(ctx, ep.apiZone, zoneID, &record) 175 if err != nil { 176 return err 177 } 178 179 break 180 } 181 } 182 183 for _, epoint := range changes.UpdateOld { 184 // Since Exoscale "Patches", we ignore UpdateOld 185 // We leave this logging here for information 186 log.Debugf("UPDATE-OLD (ignored) for epoint: %+v", epoint) 187 } 188 189 for _, epoint := range changes.Delete { 190 if !ep.domain.Match(epoint.DNSName) { 191 continue 192 } 193 194 zoneID, name := ep.filter.EndpointZoneID(epoint, zones) 195 if zoneID == "" { 196 continue 197 } 198 199 records, err := ep.client.ListDNSDomainRecords(ctx, ep.apiZone, zoneID) 200 if err != nil { 201 return err 202 } 203 204 for _, record := range records { 205 if *record.Name != name { 206 continue 207 } 208 209 err = ep.client.DeleteDNSDomainRecord(ctx, ep.apiZone, zoneID, &egoscale.DNSDomainRecord{ID: record.ID}) 210 if err != nil { 211 return err 212 } 213 214 break 215 } 216 } 217 218 return nil 219 } 220 221 // Records returns the list of endpoints 222 func (ep *ExoscaleProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { 223 ctx = exoapi.WithEndpoint(ctx, exoapi.NewReqEndpoint(ep.apiEnv, ep.apiZone)) 224 endpoints := make([]*endpoint.Endpoint, 0) 225 226 domains, err := ep.client.ListDNSDomains(ctx, ep.apiZone) 227 if err != nil { 228 return nil, err 229 } 230 231 for _, domain := range domains { 232 records, err := ep.client.ListDNSDomainRecords(ctx, ep.apiZone, *domain.ID) 233 if err != nil { 234 return nil, err 235 } 236 237 for _, record := range records { 238 switch *record.Type { 239 case "A", "CNAME", "TXT": 240 break 241 default: 242 continue 243 } 244 245 e := endpoint.NewEndpointWithTTL((*record.Name)+"."+(*domain.UnicodeName), *record.Type, endpoint.TTL(*record.TTL), *record.Content) 246 endpoints = append(endpoints, e) 247 } 248 } 249 250 log.Infof("called Records() with %d items", len(endpoints)) 251 return endpoints, nil 252 } 253 254 // ExoscaleWithDomain modifies the domain on which dns zones are filtered 255 func ExoscaleWithDomain(domainFilter endpoint.DomainFilter) ExoscaleOption { 256 return func(p *ExoscaleProvider) { 257 p.domain = domainFilter 258 } 259 } 260 261 // ExoscaleWithLogging injects logging when ApplyChanges is called 262 func ExoscaleWithLogging() ExoscaleOption { 263 return func(p *ExoscaleProvider) { 264 p.OnApplyChanges = func(changes *plan.Changes) { 265 for _, v := range changes.Create { 266 log.Infof("CREATE: %v", v) 267 } 268 for _, v := range changes.UpdateOld { 269 log.Infof("UPDATE (old): %v", v) 270 } 271 for _, v := range changes.UpdateNew { 272 log.Infof("UPDATE (new): %v", v) 273 } 274 for _, v := range changes.Delete { 275 log.Infof("DELETE: %v", v) 276 } 277 } 278 } 279 } 280 281 type zoneFilter struct { 282 domain string 283 } 284 285 // Zones filters map[zoneID]zoneName for names having f.domain as suffix 286 func (f *zoneFilter) Zones(zones map[string]string) map[string]string { 287 result := map[string]string{} 288 for zoneID, zoneName := range zones { 289 if strings.HasSuffix(zoneName, f.domain) { 290 result[zoneID] = zoneName 291 } 292 } 293 return result 294 } 295 296 // EndpointZoneID determines zoneID for endpoint from map[zoneID]zoneName by taking longest suffix zoneName match in endpoint DNSName 297 // returns empty string if no matches are found 298 func (f *zoneFilter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[string]string) (zoneID string, name string) { 299 var matchZoneID string 300 var matchZoneName string 301 for zoneID, zoneName := range zones { 302 if strings.HasSuffix(endpoint.DNSName, "."+zoneName) && len(zoneName) > len(matchZoneName) { 303 matchZoneName = zoneName 304 matchZoneID = zoneID 305 name = strings.TrimSuffix(endpoint.DNSName, "."+zoneName) 306 } 307 } 308 return matchZoneID, name 309 } 310 311 func merge(updateOld, updateNew []*endpoint.Endpoint) []*endpoint.Endpoint { 312 findMatch := func(template *endpoint.Endpoint) *endpoint.Endpoint { 313 for _, new := range updateNew { 314 if template.DNSName == new.DNSName && 315 template.RecordType == new.RecordType { 316 return new 317 } 318 } 319 return nil 320 } 321 322 var result []*endpoint.Endpoint 323 for _, old := range updateOld { 324 matchingNew := findMatch(old) 325 if matchingNew == nil { 326 // no match, shouldn't happen 327 continue 328 } 329 330 if !matchingNew.Targets.Same(old.Targets) { 331 // new target: always update, TTL will be overwritten too if necessary 332 result = append(result, matchingNew) 333 continue 334 } 335 336 if matchingNew.RecordTTL != 0 && matchingNew.RecordTTL != old.RecordTTL { 337 // same target, but new non-zero TTL set in k8s, must update 338 // probably would happen only if there is a bug in the code calling the provider 339 result = append(result, matchingNew) 340 } 341 } 342 343 return result 344 }