sigs.k8s.io/external-dns@v0.14.1/endpoint/endpoint.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 endpoint
    18  
    19  import (
    20  	"fmt"
    21  	"net/netip"
    22  	"sort"
    23  	"strings"
    24  
    25  	log "github.com/sirupsen/logrus"
    26  
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  )
    29  
    30  const (
    31  	// RecordTypeA is a RecordType enum value
    32  	RecordTypeA = "A"
    33  	// RecordTypeAAAA is a RecordType enum value
    34  	RecordTypeAAAA = "AAAA"
    35  	// RecordTypeCNAME is a RecordType enum value
    36  	RecordTypeCNAME = "CNAME"
    37  	// RecordTypeTXT is a RecordType enum value
    38  	RecordTypeTXT = "TXT"
    39  	// RecordTypeSRV is a RecordType enum value
    40  	RecordTypeSRV = "SRV"
    41  	// RecordTypeNS is a RecordType enum value
    42  	RecordTypeNS = "NS"
    43  	// RecordTypePTR is a RecordType enum value
    44  	RecordTypePTR = "PTR"
    45  	// RecordTypeMX is a RecordType enum value
    46  	RecordTypeMX = "MX"
    47  	// RecordTypeNAPTR is a RecordType enum value
    48  	RecordTypeNAPTR = "NAPTR"
    49  )
    50  
    51  // TTL is a structure defining the TTL of a DNS record
    52  type TTL int64
    53  
    54  // IsConfigured returns true if TTL is configured, false otherwise
    55  func (ttl TTL) IsConfigured() bool {
    56  	return ttl > 0
    57  }
    58  
    59  // Targets is a representation of a list of targets for an endpoint.
    60  type Targets []string
    61  
    62  // NewTargets is a convenience method to create a new Targets object from a vararg of strings
    63  func NewTargets(target ...string) Targets {
    64  	t := make(Targets, 0, len(target))
    65  	t = append(t, target...)
    66  	return t
    67  }
    68  
    69  func (t Targets) String() string {
    70  	return strings.Join(t, ";")
    71  }
    72  
    73  func (t Targets) Len() int {
    74  	return len(t)
    75  }
    76  
    77  func (t Targets) Less(i, j int) bool {
    78  	return t[i] < t[j]
    79  }
    80  
    81  func (t Targets) Swap(i, j int) {
    82  	t[i], t[j] = t[j], t[i]
    83  }
    84  
    85  // Same compares to Targets and returns true if they are identical (case-insensitive)
    86  func (t Targets) Same(o Targets) bool {
    87  	if len(t) != len(o) {
    88  		return false
    89  	}
    90  	sort.Stable(t)
    91  	sort.Stable(o)
    92  
    93  	for i, e := range t {
    94  		if !strings.EqualFold(e, o[i]) {
    95  			return false
    96  		}
    97  	}
    98  	return true
    99  }
   100  
   101  // IsLess should fulfill the requirement to compare two targets and choose the 'lesser' one.
   102  // In the past target was a simple string so simple string comparison could be used. Now we define 'less'
   103  // as either being the shorter list of targets or where the first entry is less.
   104  // FIXME We really need to define under which circumstances a list Targets is considered 'less'
   105  // than another.
   106  func (t Targets) IsLess(o Targets) bool {
   107  	if len(t) < len(o) {
   108  		return true
   109  	}
   110  	if len(t) > len(o) {
   111  		return false
   112  	}
   113  
   114  	sort.Sort(t)
   115  	sort.Sort(o)
   116  
   117  	for i, e := range t {
   118  		if e != o[i] {
   119  			// Explicitly prefers IP addresses (e.g. A records) over FQDNs (e.g. CNAMEs).
   120  			// This prevents behavior like `1-2-3-4.example.com` being "less" than `1.2.3.4` when doing lexicographical string comparison.
   121  			ipA, err := netip.ParseAddr(e)
   122  			if err != nil {
   123  				// Ignoring parsing errors is fine due to the empty netip.Addr{} type being an invalid IP,
   124  				// which is checked by IsValid() below. However, still log them in case a provider is experiencing
   125  				// non-obvious issues with the records being created.
   126  				log.WithFields(log.Fields{
   127  					"targets":           t,
   128  					"comparisonTargets": o,
   129  				}).Debugf("Couldn't parse %s as an IP address: %v", e, err)
   130  			}
   131  
   132  			ipB, err := netip.ParseAddr(o[i])
   133  			if err != nil {
   134  				log.WithFields(log.Fields{
   135  					"targets":           t,
   136  					"comparisonTargets": o,
   137  				}).Debugf("Couldn't parse %s as an IP address: %v", e, err)
   138  			}
   139  
   140  			// If both targets are valid IP addresses, use the built-in Less() function to do the comparison.
   141  			// If one is a valid IP and the other is not, prefer the IP address (consider it "less").
   142  			// If neither is a valid IP, use lexicographical string comparison to determine which string sorts first alphabetically.
   143  			switch {
   144  			case ipA.IsValid() && ipB.IsValid():
   145  				return ipA.Less(ipB)
   146  			case ipA.IsValid() && !ipB.IsValid():
   147  				return true
   148  			case !ipA.IsValid() && ipB.IsValid():
   149  				return false
   150  			default:
   151  				return e < o[i]
   152  			}
   153  		}
   154  	}
   155  	return false
   156  }
   157  
   158  // ProviderSpecificProperty holds the name and value of a configuration which is specific to individual DNS providers
   159  type ProviderSpecificProperty struct {
   160  	Name  string `json:"name,omitempty"`
   161  	Value string `json:"value,omitempty"`
   162  }
   163  
   164  // ProviderSpecific holds configuration which is specific to individual DNS providers
   165  type ProviderSpecific []ProviderSpecificProperty
   166  
   167  // EndpointKey is the type of a map key for separating endpoints or targets.
   168  type EndpointKey struct {
   169  	DNSName       string
   170  	RecordType    string
   171  	SetIdentifier string
   172  }
   173  
   174  // Endpoint is a high-level way of a connection between a service and an IP
   175  type Endpoint struct {
   176  	// The hostname of the DNS record
   177  	DNSName string `json:"dnsName,omitempty"`
   178  	// The targets the DNS record points to
   179  	Targets Targets `json:"targets,omitempty"`
   180  	// RecordType type of record, e.g. CNAME, A, AAAA, SRV, TXT etc
   181  	RecordType string `json:"recordType,omitempty"`
   182  	// Identifier to distinguish multiple records with the same name and type (e.g. Route53 records with routing policies other than 'simple')
   183  	SetIdentifier string `json:"setIdentifier,omitempty"`
   184  	// TTL for the record
   185  	RecordTTL TTL `json:"recordTTL,omitempty"`
   186  	// Labels stores labels defined for the Endpoint
   187  	// +optional
   188  	Labels Labels `json:"labels,omitempty"`
   189  	// ProviderSpecific stores provider specific config
   190  	// +optional
   191  	ProviderSpecific ProviderSpecific `json:"providerSpecific,omitempty"`
   192  }
   193  
   194  // NewEndpoint initialization method to be used to create an endpoint
   195  func NewEndpoint(dnsName, recordType string, targets ...string) *Endpoint {
   196  	return NewEndpointWithTTL(dnsName, recordType, TTL(0), targets...)
   197  }
   198  
   199  // NewEndpointWithTTL initialization method to be used to create an endpoint with a TTL struct
   200  func NewEndpointWithTTL(dnsName, recordType string, ttl TTL, targets ...string) *Endpoint {
   201  	cleanTargets := make([]string, len(targets))
   202  	for idx, target := range targets {
   203  		cleanTargets[idx] = strings.TrimSuffix(target, ".")
   204  	}
   205  
   206  	for _, label := range strings.Split(dnsName, ".") {
   207  		if len(label) > 63 {
   208  			log.Errorf("label %s in %s is longer than 63 characters. Cannot create endpoint", label, dnsName)
   209  			return nil
   210  		}
   211  	}
   212  
   213  	return &Endpoint{
   214  		DNSName:    strings.TrimSuffix(dnsName, "."),
   215  		Targets:    cleanTargets,
   216  		RecordType: recordType,
   217  		Labels:     NewLabels(),
   218  		RecordTTL:  ttl,
   219  	}
   220  }
   221  
   222  // WithSetIdentifier applies the given set identifier to the endpoint.
   223  func (e *Endpoint) WithSetIdentifier(setIdentifier string) *Endpoint {
   224  	e.SetIdentifier = setIdentifier
   225  	return e
   226  }
   227  
   228  // WithProviderSpecific attaches a key/value pair to the Endpoint and returns the Endpoint.
   229  // This can be used to pass additional data through the stages of ExternalDNS's Endpoint processing.
   230  // The assumption is that most of the time this will be provider specific metadata that doesn't
   231  // warrant its own field on the Endpoint object itself. It differs from Labels in the fact that it's
   232  // not persisted in the Registry but only kept in memory during a single record synchronization.
   233  func (e *Endpoint) WithProviderSpecific(key, value string) *Endpoint {
   234  	e.SetProviderSpecificProperty(key, value)
   235  	return e
   236  }
   237  
   238  // GetProviderSpecificProperty returns the value of a ProviderSpecificProperty if the property exists.
   239  func (e *Endpoint) GetProviderSpecificProperty(key string) (string, bool) {
   240  	for _, providerSpecific := range e.ProviderSpecific {
   241  		if providerSpecific.Name == key {
   242  			return providerSpecific.Value, true
   243  		}
   244  	}
   245  	return "", false
   246  }
   247  
   248  // SetProviderSpecificProperty sets the value of a ProviderSpecificProperty.
   249  func (e *Endpoint) SetProviderSpecificProperty(key string, value string) {
   250  	for i, providerSpecific := range e.ProviderSpecific {
   251  		if providerSpecific.Name == key {
   252  			e.ProviderSpecific[i] = ProviderSpecificProperty{
   253  				Name:  key,
   254  				Value: value,
   255  			}
   256  			return
   257  		}
   258  	}
   259  
   260  	e.ProviderSpecific = append(e.ProviderSpecific, ProviderSpecificProperty{Name: key, Value: value})
   261  }
   262  
   263  // DeleteProviderSpecificProperty deletes any ProviderSpecificProperty of the specified name.
   264  func (e *Endpoint) DeleteProviderSpecificProperty(key string) {
   265  	for i, providerSpecific := range e.ProviderSpecific {
   266  		if providerSpecific.Name == key {
   267  			e.ProviderSpecific = append(e.ProviderSpecific[:i], e.ProviderSpecific[i+1:]...)
   268  			return
   269  		}
   270  	}
   271  }
   272  
   273  // Key returns the EndpointKey of the Endpoint.
   274  func (e *Endpoint) Key() EndpointKey {
   275  	return EndpointKey{
   276  		DNSName:       e.DNSName,
   277  		RecordType:    e.RecordType,
   278  		SetIdentifier: e.SetIdentifier,
   279  	}
   280  }
   281  
   282  // IsOwnedBy returns true if the endpoint owner label matches the given ownerID, false otherwise
   283  func (e *Endpoint) IsOwnedBy(ownerID string) bool {
   284  	endpointOwner, ok := e.Labels[OwnerLabelKey]
   285  	return ok && endpointOwner == ownerID
   286  }
   287  
   288  func (e *Endpoint) String() string {
   289  	return fmt.Sprintf("%s %d IN %s %s %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.SetIdentifier, e.Targets, e.ProviderSpecific)
   290  }
   291  
   292  // Apply filter to slice of endpoints and return new filtered slice that includes
   293  // only endpoints that match.
   294  func FilterEndpointsByOwnerID(ownerID string, eps []*Endpoint) []*Endpoint {
   295  	filtered := []*Endpoint{}
   296  	for _, ep := range eps {
   297  		if endpointOwner, ok := ep.Labels[OwnerLabelKey]; !ok || endpointOwner != ownerID {
   298  			log.Debugf(`Skipping endpoint %v because owner id does not match, found: "%s", required: "%s"`, ep, endpointOwner, ownerID)
   299  		} else {
   300  			filtered = append(filtered, ep)
   301  		}
   302  	}
   303  
   304  	return filtered
   305  }
   306  
   307  // DNSEndpointSpec defines the desired state of DNSEndpoint
   308  type DNSEndpointSpec struct {
   309  	Endpoints []*Endpoint `json:"endpoints,omitempty"`
   310  }
   311  
   312  // DNSEndpointStatus defines the observed state of DNSEndpoint
   313  type DNSEndpointStatus struct {
   314  	// The generation observed by the external-dns controller.
   315  	// +optional
   316  	ObservedGeneration int64 `json:"observedGeneration,omitempty"`
   317  }
   318  
   319  // +genclient
   320  // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
   321  
   322  // DNSEndpoint is a contract that a user-specified CRD must implement to be used as a source for external-dns.
   323  // The user-specified CRD should also have the status sub-resource.
   324  // +k8s:openapi-gen=true
   325  // +groupName=externaldns.k8s.io
   326  // +kubebuilder:resource:path=dnsendpoints
   327  // +kubebuilder:object:root=true
   328  // +kubebuilder:subresource:status
   329  // +versionName=v1alpha1
   330  
   331  type DNSEndpoint struct {
   332  	metav1.TypeMeta   `json:",inline"`
   333  	metav1.ObjectMeta `json:"metadata,omitempty"`
   334  
   335  	Spec   DNSEndpointSpec   `json:"spec,omitempty"`
   336  	Status DNSEndpointStatus `json:"status,omitempty"`
   337  }
   338  
   339  // +kubebuilder:object:root=true
   340  // DNSEndpointList is a list of DNSEndpoint objects
   341  type DNSEndpointList struct {
   342  	metav1.TypeMeta `json:",inline"`
   343  	metav1.ListMeta `json:"metadata,omitempty"`
   344  	Items           []DNSEndpoint `json:"items"`
   345  }