sigs.k8s.io/external-dns@v0.14.1/provider/inmemory/inmemory.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 inmemory 18 19 import ( 20 "context" 21 "errors" 22 "strings" 23 24 log "github.com/sirupsen/logrus" 25 "k8s.io/apimachinery/pkg/util/sets" 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 var ( 33 // ErrZoneAlreadyExists error returned when zone cannot be created when it already exists 34 ErrZoneAlreadyExists = errors.New("specified zone already exists") 35 // ErrZoneNotFound error returned when specified zone does not exists 36 ErrZoneNotFound = errors.New("specified zone not found") 37 // ErrRecordAlreadyExists when create request is sent but record already exists 38 ErrRecordAlreadyExists = errors.New("record already exists") 39 // ErrRecordNotFound when update/delete request is sent but record not found 40 ErrRecordNotFound = errors.New("record not found") 41 // ErrDuplicateRecordFound when record is repeated in create/update/delete 42 ErrDuplicateRecordFound = errors.New("invalid batch request") 43 ) 44 45 // InMemoryProvider - dns provider only used for testing purposes 46 // initialized as dns provider with no records 47 type InMemoryProvider struct { 48 provider.BaseProvider 49 domain endpoint.DomainFilter 50 client *inMemoryClient 51 filter *filter 52 OnApplyChanges func(ctx context.Context, changes *plan.Changes) 53 OnRecords func() 54 } 55 56 // InMemoryOption allows to extend in-memory provider 57 type InMemoryOption func(*InMemoryProvider) 58 59 // InMemoryWithLogging injects logging when ApplyChanges is called 60 func InMemoryWithLogging() InMemoryOption { 61 return func(p *InMemoryProvider) { 62 p.OnApplyChanges = func(ctx context.Context, changes *plan.Changes) { 63 for _, v := range changes.Create { 64 log.Infof("CREATE: %v", v) 65 } 66 for _, v := range changes.UpdateOld { 67 log.Infof("UPDATE (old): %v", v) 68 } 69 for _, v := range changes.UpdateNew { 70 log.Infof("UPDATE (new): %v", v) 71 } 72 for _, v := range changes.Delete { 73 log.Infof("DELETE: %v", v) 74 } 75 } 76 } 77 } 78 79 // InMemoryWithDomain modifies the domain on which dns zones are filtered 80 func InMemoryWithDomain(domainFilter endpoint.DomainFilter) InMemoryOption { 81 return func(p *InMemoryProvider) { 82 p.domain = domainFilter 83 } 84 } 85 86 // InMemoryInitZones pre-seeds the InMemoryProvider with given zones 87 func InMemoryInitZones(zones []string) InMemoryOption { 88 return func(p *InMemoryProvider) { 89 for _, z := range zones { 90 if err := p.CreateZone(z); err != nil { 91 log.Warnf("Unable to initialize zones for inmemory provider") 92 } 93 } 94 } 95 } 96 97 // NewInMemoryProvider returns InMemoryProvider DNS provider interface implementation 98 func NewInMemoryProvider(opts ...InMemoryOption) *InMemoryProvider { 99 im := &InMemoryProvider{ 100 filter: &filter{}, 101 OnApplyChanges: func(ctx context.Context, changes *plan.Changes) {}, 102 OnRecords: func() {}, 103 domain: endpoint.NewDomainFilter([]string{""}), 104 client: newInMemoryClient(), 105 } 106 107 for _, opt := range opts { 108 opt(im) 109 } 110 111 return im 112 } 113 114 // CreateZone adds new zone if not present 115 func (im *InMemoryProvider) CreateZone(newZone string) error { 116 return im.client.CreateZone(newZone) 117 } 118 119 // Zones returns filtered zones as specified by domain 120 func (im *InMemoryProvider) Zones() map[string]string { 121 return im.filter.Zones(im.client.Zones()) 122 } 123 124 // Records returns the list of endpoints 125 func (im *InMemoryProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { 126 defer im.OnRecords() 127 128 endpoints := make([]*endpoint.Endpoint, 0) 129 130 for zoneID := range im.Zones() { 131 records, err := im.client.Records(zoneID) 132 if err != nil { 133 return nil, err 134 } 135 136 endpoints = append(endpoints, copyEndpoints(records)...) 137 } 138 139 return endpoints, nil 140 } 141 142 // ApplyChanges simply modifies records in memory 143 // error checking occurs before any modifications are made, i.e. batch processing 144 // create record - record should not exist 145 // update/delete record - record should exist 146 // create/update/delete lists should not have overlapping records 147 func (im *InMemoryProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 148 defer im.OnApplyChanges(ctx, changes) 149 150 perZoneChanges := map[string]*plan.Changes{} 151 152 zones := im.Zones() 153 for zoneID := range zones { 154 perZoneChanges[zoneID] = &plan.Changes{} 155 } 156 157 for _, ep := range changes.Create { 158 zoneID := im.filter.EndpointZoneID(ep, zones) 159 if zoneID == "" { 160 continue 161 } 162 perZoneChanges[zoneID].Create = append(perZoneChanges[zoneID].Create, ep) 163 } 164 for _, ep := range changes.UpdateNew { 165 zoneID := im.filter.EndpointZoneID(ep, zones) 166 if zoneID == "" { 167 continue 168 } 169 perZoneChanges[zoneID].UpdateNew = append(perZoneChanges[zoneID].UpdateNew, ep) 170 } 171 for _, ep := range changes.UpdateOld { 172 zoneID := im.filter.EndpointZoneID(ep, zones) 173 if zoneID == "" { 174 continue 175 } 176 perZoneChanges[zoneID].UpdateOld = append(perZoneChanges[zoneID].UpdateOld, ep) 177 } 178 for _, ep := range changes.Delete { 179 zoneID := im.filter.EndpointZoneID(ep, zones) 180 if zoneID == "" { 181 continue 182 } 183 perZoneChanges[zoneID].Delete = append(perZoneChanges[zoneID].Delete, ep) 184 } 185 186 for zoneID := range perZoneChanges { 187 change := &plan.Changes{ 188 Create: perZoneChanges[zoneID].Create, 189 UpdateNew: perZoneChanges[zoneID].UpdateNew, 190 UpdateOld: perZoneChanges[zoneID].UpdateOld, 191 Delete: perZoneChanges[zoneID].Delete, 192 } 193 err := im.client.ApplyChanges(ctx, zoneID, change) 194 if err != nil { 195 return err 196 } 197 } 198 199 return nil 200 } 201 202 func copyEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint { 203 records := make([]*endpoint.Endpoint, 0, len(endpoints)) 204 for _, ep := range endpoints { 205 newEp := endpoint.NewEndpointWithTTL(ep.DNSName, ep.RecordType, ep.RecordTTL, ep.Targets...).WithSetIdentifier(ep.SetIdentifier) 206 newEp.Labels = endpoint.NewLabels() 207 for k, v := range ep.Labels { 208 newEp.Labels[k] = v 209 } 210 newEp.ProviderSpecific = append(endpoint.ProviderSpecific(nil), ep.ProviderSpecific...) 211 records = append(records, newEp) 212 } 213 return records 214 } 215 216 type filter struct { 217 domain string 218 } 219 220 // Zones filters map[zoneID]zoneName for names having f.domain as suffix 221 func (f *filter) Zones(zones map[string]string) map[string]string { 222 result := map[string]string{} 223 for zoneID, zoneName := range zones { 224 if strings.HasSuffix(zoneName, f.domain) { 225 result[zoneID] = zoneName 226 } 227 } 228 return result 229 } 230 231 // EndpointZoneID determines zoneID for endpoint from map[zoneID]zoneName by taking longest suffix zoneName match in endpoint DNSName 232 // returns empty string if no match found 233 func (f *filter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[string]string) (zoneID string) { 234 var matchZoneID, matchZoneName string 235 for zoneID, zoneName := range zones { 236 if strings.HasSuffix(endpoint.DNSName, zoneName) && len(zoneName) > len(matchZoneName) { 237 matchZoneName = zoneName 238 matchZoneID = zoneID 239 } 240 } 241 return matchZoneID 242 } 243 244 type zone map[endpoint.EndpointKey]*endpoint.Endpoint 245 246 type inMemoryClient struct { 247 zones map[string]zone 248 } 249 250 func newInMemoryClient() *inMemoryClient { 251 return &inMemoryClient{map[string]zone{}} 252 } 253 254 func (c *inMemoryClient) Records(zone string) ([]*endpoint.Endpoint, error) { 255 if _, ok := c.zones[zone]; !ok { 256 return nil, ErrZoneNotFound 257 } 258 259 var records []*endpoint.Endpoint 260 for _, rec := range c.zones[zone] { 261 records = append(records, rec) 262 } 263 return records, nil 264 } 265 266 func (c *inMemoryClient) Zones() map[string]string { 267 zones := map[string]string{} 268 for zone := range c.zones { 269 zones[zone] = zone 270 } 271 return zones 272 } 273 274 func (c *inMemoryClient) CreateZone(zone string) error { 275 if _, ok := c.zones[zone]; ok { 276 return ErrZoneAlreadyExists 277 } 278 c.zones[zone] = map[endpoint.EndpointKey]*endpoint.Endpoint{} 279 280 return nil 281 } 282 283 func (c *inMemoryClient) ApplyChanges(ctx context.Context, zoneID string, changes *plan.Changes) error { 284 if err := c.validateChangeBatch(zoneID, changes); err != nil { 285 return err 286 } 287 for _, newEndpoint := range changes.Create { 288 c.zones[zoneID][newEndpoint.Key()] = newEndpoint 289 } 290 for _, updateEndpoint := range changes.UpdateNew { 291 c.zones[zoneID][updateEndpoint.Key()] = updateEndpoint 292 } 293 for _, deleteEndpoint := range changes.Delete { 294 delete(c.zones[zoneID], deleteEndpoint.Key()) 295 } 296 return nil 297 } 298 299 func (c *inMemoryClient) updateMesh(mesh sets.Set[endpoint.EndpointKey], record *endpoint.Endpoint) error { 300 if mesh.Has(record.Key()) { 301 return ErrDuplicateRecordFound 302 } 303 mesh.Insert(record.Key()) 304 return nil 305 } 306 307 // validateChangeBatch validates that the changes passed to InMemory DNS provider is valid 308 func (c *inMemoryClient) validateChangeBatch(zone string, changes *plan.Changes) error { 309 curZone, ok := c.zones[zone] 310 if !ok { 311 return ErrZoneNotFound 312 } 313 mesh := sets.New[endpoint.EndpointKey]() 314 for _, newEndpoint := range changes.Create { 315 if _, exists := curZone[newEndpoint.Key()]; exists { 316 return ErrRecordAlreadyExists 317 } 318 if err := c.updateMesh(mesh, newEndpoint); err != nil { 319 return err 320 } 321 } 322 for _, updateEndpoint := range changes.UpdateNew { 323 if _, exists := curZone[updateEndpoint.Key()]; !exists { 324 return ErrRecordNotFound 325 } 326 if err := c.updateMesh(mesh, updateEndpoint); err != nil { 327 return err 328 } 329 } 330 for _, updateOldEndpoint := range changes.UpdateOld { 331 if rec, exists := curZone[updateOldEndpoint.Key()]; !exists || rec.Targets[0] != updateOldEndpoint.Targets[0] { 332 return ErrRecordNotFound 333 } 334 } 335 for _, deleteEndpoint := range changes.Delete { 336 if rec, exists := curZone[deleteEndpoint.Key()]; !exists || rec.Targets[0] != deleteEndpoint.Targets[0] { 337 return ErrRecordNotFound 338 } 339 if err := c.updateMesh(mesh, deleteEndpoint); err != nil { 340 return err 341 } 342 } 343 return nil 344 }