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 }