sigs.k8s.io/external-dns@v0.14.1/provider/rcode0/rcode0.go (about) 1 /* 2 Copyright 2019 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 rcode0 18 19 import ( 20 "context" 21 "fmt" 22 "net/url" 23 "os" 24 "strings" 25 26 rc0 "github.com/nic-at/rc0go" 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 // RcodeZeroProvider implements the DNS provider for RcodeZero Anycast DNS. 35 type RcodeZeroProvider struct { 36 provider.BaseProvider 37 Client *rc0.Client 38 39 DomainFilter endpoint.DomainFilter 40 DryRun bool 41 TXTEncrypt bool 42 Key []byte 43 } 44 45 // NewRcodeZeroProvider creates a new RcodeZero Anycast DNS provider. 46 // 47 // Returns the provider or an error if a provider could not be created. 48 func NewRcodeZeroProvider(domainFilter endpoint.DomainFilter, dryRun bool, txtEnc bool) (*RcodeZeroProvider, error) { 49 client, err := rc0.NewClient(os.Getenv("RC0_API_KEY")) 50 if err != nil { 51 return nil, err 52 } 53 54 value := os.Getenv("RC0_BASE_URL") 55 if len(value) != 0 { 56 client.BaseURL, err = url.Parse(os.Getenv("RC0_BASE_URL")) 57 } 58 59 if err != nil { 60 return nil, fmt.Errorf("failed to initialize rcodezero provider: %v", err) 61 } 62 63 provider := &RcodeZeroProvider{ 64 Client: client, 65 DomainFilter: domainFilter, 66 DryRun: dryRun, 67 TXTEncrypt: txtEnc, 68 } 69 70 if txtEnc { 71 provider.Key = []byte(os.Getenv("RC0_ENC_KEY")) 72 } 73 74 return provider, nil 75 } 76 77 // Zones returns filtered zones if filter is set 78 func (p *RcodeZeroProvider) Zones() ([]*rc0.Zone, error) { 79 var result []*rc0.Zone 80 81 zones, err := p.fetchZones() 82 if err != nil { 83 return nil, err 84 } 85 86 for _, zone := range zones { 87 if p.DomainFilter.Match(zone.Domain) { 88 result = append(result, zone) 89 } 90 } 91 92 return result, nil 93 } 94 95 // Records returns resource records 96 // 97 // Decrypts TXT records if TXT-Encrypt flag is set and key is provided 98 func (p *RcodeZeroProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { 99 zones, err := p.Zones() 100 if err != nil { 101 return nil, err 102 } 103 104 var endpoints []*endpoint.Endpoint 105 106 for _, zone := range zones { 107 rrset, err := p.fetchRecords(zone.Domain) 108 if err != nil { 109 return nil, err 110 } 111 112 for _, r := range rrset { 113 if provider.SupportedRecordType(r.Type) { 114 if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(r.Type, "TXT") { 115 p.Client.RRSet.DecryptTXT(p.Key, r) 116 } 117 if len(r.Records) > 1 { 118 for _, _r := range r.Records { 119 if !_r.Disabled { 120 endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), _r.Content)) 121 } 122 } 123 } else if !r.Records[0].Disabled { 124 endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), r.Records[0].Content)) 125 } 126 } 127 } 128 } 129 130 return endpoints, nil 131 } 132 133 // ApplyChanges applies a given set of changes in a given zone. 134 func (p *RcodeZeroProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 135 combinedChanges := make([]*rc0.RRSetChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) 136 137 combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeADD, changes.Create)...) 138 combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeUPDATE, changes.UpdateNew)...) 139 combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeDELETE, changes.Delete)...) 140 141 return p.submitChanges(combinedChanges) 142 } 143 144 // Helper function 145 func rcodezeroChangesByZone(zones []*rc0.Zone, changeSet []*rc0.RRSetChange) map[string][]*rc0.RRSetChange { 146 changes := make(map[string][]*rc0.RRSetChange) 147 zoneNameIDMapper := provider.ZoneIDName{} 148 for _, z := range zones { 149 zoneNameIDMapper.Add(z.Domain, z.Domain) 150 changes[z.Domain] = []*rc0.RRSetChange{} 151 } 152 153 for _, c := range changeSet { 154 zone, _ := zoneNameIDMapper.FindZone(c.Name) 155 if zone == "" { 156 log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.Name) 157 continue 158 } 159 changes[zone] = append(changes[zone], c) 160 } 161 162 return changes 163 } 164 165 // Helper function 166 func (p *RcodeZeroProvider) fetchRecords(zoneName string) ([]*rc0.RRType, error) { 167 var allRecords []*rc0.RRType 168 169 listOptions := rc0.NewListOptions() 170 171 for { 172 records, page, err := p.Client.RRSet.List(zoneName, listOptions) 173 if err != nil { 174 return nil, err 175 } 176 177 allRecords = append(allRecords, records...) 178 179 if page == nil || (page.CurrentPage == page.LastPage) { 180 break 181 } 182 183 listOptions.SetPageNumber(page.CurrentPage + 1) 184 } 185 186 return allRecords, nil 187 } 188 189 // Helper function 190 func (p *RcodeZeroProvider) fetchZones() ([]*rc0.Zone, error) { 191 var allZones []*rc0.Zone 192 193 listOptions := rc0.NewListOptions() 194 195 for { 196 zones, page, err := p.Client.Zones.List(listOptions) 197 if err != nil { 198 return nil, err 199 } 200 allZones = append(allZones, zones...) 201 202 if page == nil || page.IsLastPage() { 203 break 204 } 205 206 listOptions.SetPageNumber(page.CurrentPage + 1) 207 } 208 209 return allZones, nil 210 } 211 212 // Helper function to submit changes. 213 // 214 // Changes are submitted by change type. 215 func (p *RcodeZeroProvider) submitChanges(changes []*rc0.RRSetChange) error { 216 if len(changes) == 0 { 217 return nil 218 } 219 220 zones, err := p.Zones() 221 if err != nil { 222 return err 223 } 224 225 // separate into per-zone change sets to be passed to the API. 226 changesByZone := rcodezeroChangesByZone(zones, changes) 227 for zoneName, changes := range changesByZone { 228 for _, change := range changes { 229 logFields := log.Fields{ 230 "record": change.Name, 231 "content": change.Records[0].Content, 232 "type": change.Type, 233 "action": change.ChangeType, 234 "zone": zoneName, 235 } 236 237 log.WithFields(logFields).Info("Changing record.") 238 239 if p.DryRun { 240 continue 241 } 242 243 // to avoid accidentally adding extra dot if already present 244 change.Name = strings.TrimSuffix(change.Name, ".") + "." 245 246 switch change.ChangeType { 247 case rc0.ChangeTypeADD: 248 sr, err := p.Client.RRSet.Create(zoneName, []*rc0.RRSetChange{change}) 249 if err != nil { 250 return err 251 } 252 253 if sr.HasError() { 254 return fmt.Errorf("adding new RR resulted in an error: %v", sr.Message) 255 } 256 257 case rc0.ChangeTypeUPDATE: 258 sr, err := p.Client.RRSet.Edit(zoneName, []*rc0.RRSetChange{change}) 259 if err != nil { 260 return err 261 } 262 263 if sr.HasError() { 264 return fmt.Errorf("updating existing RR resulted in an error: %v", sr.Message) 265 } 266 267 case rc0.ChangeTypeDELETE: 268 sr, err := p.Client.RRSet.Delete(zoneName, []*rc0.RRSetChange{change}) 269 if err != nil { 270 return err 271 } 272 273 if sr.HasError() { 274 return fmt.Errorf("deleting existing RR resulted in an error: %v", sr.Message) 275 } 276 277 default: 278 return fmt.Errorf("unsupported changeType submitted: %v", change.ChangeType) 279 } 280 } 281 } 282 return nil 283 } 284 285 // NewRcodezeroChanges returns a RcodeZero specific array with rrset change objects. 286 func (p *RcodeZeroProvider) NewRcodezeroChanges(action string, endpoints []*endpoint.Endpoint) []*rc0.RRSetChange { 287 changes := make([]*rc0.RRSetChange, 0, len(endpoints)) 288 289 for _, _endpoint := range endpoints { 290 changes = append(changes, p.NewRcodezeroChange(action, _endpoint)) 291 } 292 293 return changes 294 } 295 296 // NewRcodezeroChange returns a RcodeZero specific rrset change object. 297 func (p *RcodeZeroProvider) NewRcodezeroChange(action string, endpoint *endpoint.Endpoint) *rc0.RRSetChange { 298 change := &rc0.RRSetChange{ 299 Type: endpoint.RecordType, 300 ChangeType: action, 301 Name: endpoint.DNSName, 302 Records: []*rc0.Record{{ 303 Disabled: false, 304 Content: endpoint.Targets[0], 305 }}, 306 } 307 308 if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(endpoint.RecordType, "TXT") { 309 p.Client.RRSet.EncryptTXT(p.Key, change) 310 } 311 312 return change 313 }