istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/config/labels/instance.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package labels
    16  
    17  import (
    18  	"fmt"
    19  	"regexp"
    20  	"strings"
    21  
    22  	"github.com/hashicorp/go-multierror"
    23  
    24  	"istio.io/istio/pkg/maps"
    25  	"istio.io/istio/pkg/slices"
    26  )
    27  
    28  const (
    29  	DNS1123LabelMaxLength = 63 // Public for testing only.
    30  	dns1123LabelFmt       = "[a-zA-Z0-9](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?"
    31  	// a wild-card prefix is an '*', a normal DNS1123 label with a leading '*' or '*-', or a normal DNS1123 label
    32  	wildcardPrefix = `(\*|(\*|\*-)?` + dns1123LabelFmt + `)`
    33  
    34  	// Using kubernetes requirement, a valid key must be a non-empty string consist
    35  	// of alphanumeric characters, '-', '_' or '.', and must start and end with an
    36  	// alphanumeric character (e.g. 'MyValue',  or 'my_value',  or '12345'
    37  	qualifiedNameFmt = "(?:[A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]"
    38  
    39  	// In Kubernetes, label names can start with a DNS name followed by a '/':
    40  	dnsNamePrefixFmt       = dns1123LabelFmt + `(?:\.` + dns1123LabelFmt + `)*/`
    41  	dnsNamePrefixMaxLength = 253
    42  )
    43  
    44  var (
    45  	tagRegexp            = regexp.MustCompile("^(" + dnsNamePrefixFmt + ")?(" + qualifiedNameFmt + ")$") // label value can be an empty string
    46  	labelValueRegexp     = regexp.MustCompile("^" + "(" + qualifiedNameFmt + ")?" + "$")
    47  	dns1123LabelRegexp   = regexp.MustCompile("^" + dns1123LabelFmt + "$")
    48  	wildcardPrefixRegexp = regexp.MustCompile("^" + wildcardPrefix + "$")
    49  )
    50  
    51  // Instance is a non empty map of arbitrary strings. Each version of a service can
    52  // be differentiated by a unique set of labels associated with the version. These
    53  // labels are assigned to all instances of a particular service version. For
    54  // example, lets say catalog.mystore.com has 2 versions v1 and v2. v1 instances
    55  // could have labels gitCommit=aeiou234, region=us-east, while v2 instances could
    56  // have labels name=kittyCat,region=us-east.
    57  type Instance map[string]string
    58  
    59  // SubsetOf is true if the label has same values for the keys
    60  func (i Instance) SubsetOf(that Instance) bool {
    61  	if len(i) == 0 {
    62  		return true
    63  	}
    64  
    65  	if len(that) == 0 || len(that) < len(i) {
    66  		return false
    67  	}
    68  
    69  	for k, v1 := range i {
    70  		if v2, ok := that[k]; !ok || v1 != v2 {
    71  			return false
    72  		}
    73  	}
    74  	return true
    75  }
    76  
    77  // Match is true if the label has same values for the keys.
    78  // if len(i) == 0, will return false. It is mainly used for service -> workload
    79  func (i Instance) Match(that Instance) bool {
    80  	if len(i) == 0 {
    81  		return false
    82  	}
    83  
    84  	return i.SubsetOf(that)
    85  }
    86  
    87  // Equals returns true if the labels are equal.
    88  func (i Instance) Equals(that Instance) bool {
    89  	return maps.Equal(i, that)
    90  }
    91  
    92  // Validate ensures tag is well-formed
    93  func (i Instance) Validate() error {
    94  	if i == nil {
    95  		return nil
    96  	}
    97  	var errs error
    98  	for k, v := range i {
    99  		if err := validateTagKey(k); err != nil {
   100  			errs = multierror.Append(errs, err)
   101  		}
   102  		if !labelValueRegexp.MatchString(v) {
   103  			errs = multierror.Append(errs, fmt.Errorf("invalid tag value: %q", v))
   104  		}
   105  	}
   106  	return errs
   107  }
   108  
   109  // IsDNS1123Label tests for a string that conforms to the definition of a label in
   110  // DNS (RFC 1123).
   111  func IsDNS1123Label(value string) bool {
   112  	return len(value) <= DNS1123LabelMaxLength && dns1123LabelRegexp.MatchString(value)
   113  }
   114  
   115  // IsWildcardDNS1123Label tests for a string that conforms to the definition of a label in DNS (RFC 1123), but allows
   116  // the wildcard label (`*`), and typical labels with a leading astrisk instead of alphabetic character (e.g. "*-foo")
   117  func IsWildcardDNS1123Label(value string) bool {
   118  	return len(value) <= DNS1123LabelMaxLength && wildcardPrefixRegexp.MatchString(value)
   119  }
   120  
   121  // validateTagKey checks that a string is valid as a Kubernetes label name.
   122  func validateTagKey(k string) error {
   123  	match := tagRegexp.FindStringSubmatch(k)
   124  	if match == nil {
   125  		return fmt.Errorf("invalid tag key: %q", k)
   126  	}
   127  
   128  	if len(match[1]) > 0 {
   129  		dnsPrefixLength := len(match[1]) - 1 // exclude the trailing / from the length
   130  		if dnsPrefixLength > dnsNamePrefixMaxLength {
   131  			return fmt.Errorf("invalid tag key: %q (DNS prefix is too long)", k)
   132  		}
   133  	}
   134  
   135  	if len(match[2]) > DNS1123LabelMaxLength {
   136  		return fmt.Errorf("invalid tag key: %q (name is too long)", k)
   137  	}
   138  
   139  	return nil
   140  }
   141  
   142  func (i Instance) String() string {
   143  	// Ensure stable ordering
   144  	keys := slices.Sort(maps.Keys(i))
   145  
   146  	var buffer strings.Builder
   147  	// Assume each kv pair is roughly 25 characters. We could be under or over, this is just a guess to optimize
   148  	buffer.Grow(len(keys) * 25)
   149  	first := true
   150  	for _, k := range keys {
   151  		v := i[k]
   152  		if !first {
   153  			buffer.WriteString(",")
   154  		} else {
   155  			first = false
   156  		}
   157  		if len(v) > 0 {
   158  			buffer.WriteString(k + "=" + v)
   159  		} else {
   160  			buffer.WriteString(k)
   161  		}
   162  	}
   163  	return buffer.String()
   164  }