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  }