blob: 6b95db96d341a9bef402d09996a5054ae40825bd [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Serge Bazanski1f789542024-05-22 14:01:50 +02004package node
5
6import (
7 "fmt"
8 "regexp"
Serge Bazanskidd2b80f2024-09-24 13:06:27 +00009 "strings"
10
Serge Bazanski53458ba2024-06-18 09:56:46 +000011 cpb "source.monogon.dev/metropolis/proto/common"
Serge Bazanski1f789542024-05-22 14:01:50 +020012)
13
14var (
15 reLabelFirstLast = regexp.MustCompile(`^[a-zA-Z0-9]$`)
16 reLabelBody = regexp.MustCompile(`^[a-zA-Z0-9\-._]*$`)
17
18 // ErrLabelEmpty is returned by ValidateLabel if the label key/value is not at
19 // least one character long.
20 ErrLabelEmpty = fmt.Errorf("empty")
21 // ErrLabelTooLong is returned by ValidateLabel if the label key/value is more
22 // than 63 characters long.
23 ErrLabelTooLong = fmt.Errorf("too long")
24 // ErrLabelInvalidFirstCharacter is returned by ValidateLabel if the label
25 // key/value contains an invalid character on the first position.
26 ErrLabelInvalidFirstCharacter = fmt.Errorf("first character not a letter or number")
27 // ErrLabelInvalidLastCharacter is returned by ValidateLabel if the label
28 // key/value contains an invalid character on the last position.
29 ErrLabelInvalidLastCharacter = fmt.Errorf("last character not a letter or number")
30 // ErrLabelInvalidCharacter is returned by ValidateLabel if the label key/value
31 // contains an invalid character.
32 ErrLabelInvalidCharacter = fmt.Errorf("invalid character")
Serge Bazanskidd2b80f2024-09-24 13:06:27 +000033 ErrLabelEmptyPrefix = fmt.Errorf("empty prefix")
34 ErrLabelInvalidPrefix = fmt.Errorf("invalid prefix")
Serge Bazanski1f789542024-05-22 14:01:50 +020035)
36
37const (
38 // MaxLabelsPerNode is the absolute maximum of labels that can be attached to a
39 // node.
40 MaxLabelsPerNode = 128
41)
42
Serge Bazanskidd2b80f2024-09-24 13:06:27 +000043func validatePrefix(prefix string) error {
44 if prefix == "" {
45 return ErrLabelEmptyPrefix
46 }
Jan Schär690c42d2024-11-21 12:10:53 +010047 if err := validateDomainName(prefix); err != nil {
48 return fmt.Errorf("invalid prefix: %w", err)
Serge Bazanskidd2b80f2024-09-24 13:06:27 +000049 }
50 return nil
51}
52
53// ValidateLabelKey ensures that a given node label key is valid:
Serge Bazanski1f789542024-05-22 14:01:50 +020054//
55// 1. 1 to 63 characters long (inclusive);
56// 2. Characters are all ASCII a-z A-Z 0-9 '_', '-' or '.';
57// 3. The first character is ASCII a-z A-Z or 0-9.
58// 4. The last character is ASCII a-z A-Z or 0-9.
Serge Bazanskidd2b80f2024-09-24 13:06:27 +000059// 5. Optional slash-delimited prefix which contains a valid 'DNS subdomain'.
Serge Bazanski1f789542024-05-22 14:01:50 +020060//
61// If it's valid, nil is returned. Otherwise, one of ErrLabelEmpty,
62// ErrLabelTooLong, ErrLabelInvalidFirstCharacter or ErrLabelInvalidCharacter is
63// returned.
Serge Bazanskidd2b80f2024-09-24 13:06:27 +000064func ValidateLabelKey(v string) error {
65 // Split away prefix.
66 parts := strings.Split(v, "/")
67 switch len(parts) {
68 case 1:
69 case 2:
70 prefix := parts[0]
71 if err := validatePrefix(prefix); err != nil {
72 return err
73 }
74 v = parts[1]
75 default:
76 return ErrLabelInvalidPrefix
77 }
78
Serge Bazanski1f789542024-05-22 14:01:50 +020079 if len(v) == 0 {
80 return ErrLabelEmpty
81 }
Serge Bazanskidd2b80f2024-09-24 13:06:27 +000082
Serge Bazanski1f789542024-05-22 14:01:50 +020083 if len(v) > 63 {
84 return ErrLabelTooLong
85 }
86 if !reLabelFirstLast.MatchString(string(v[0])) {
87 return ErrLabelInvalidFirstCharacter
88 }
89 if !reLabelFirstLast.MatchString(string(v[len(v)-1])) {
90 return ErrLabelInvalidLastCharacter
91 }
92 // Body characters are a superset of the first/last characters, and we've already
93 // checked those so we can check the entire string here.
94 if !reLabelBody.MatchString(v) {
95 return ErrLabelInvalidCharacter
96 }
97 return nil
98}
Serge Bazanski53458ba2024-06-18 09:56:46 +000099
Serge Bazanskidd2b80f2024-09-24 13:06:27 +0000100// ValidateLabelValue ensures that a given node label value is valid:
101//
102// 1. 0 to 63 characters long (inclusive);
103// 2. Characters are all ASCII a-z A-Z 0-9 '_', '-' or '.';
104// 3. The first character is ASCII a-z A-Z or 0-9.
105// 4. The last character is ASCII a-z A-Z or 0-9.
106//
107// If it's valid, nil is returned. Otherwise, one of ErrLabelTooLong,
108// ErrLabelInvalidFirstCharacter, or ErrLabelInvalidLastCharacter,
109// ErrLabelInvalidCharacter is returned.
110func ValidateLabelValue(v string) error {
111 if len(v) == 0 {
112 return nil
113 }
114 if len(v) > 63 {
115 return ErrLabelTooLong
116 }
117 if !reLabelFirstLast.MatchString(string(v[0])) {
118 return ErrLabelInvalidFirstCharacter
119 }
120 if !reLabelFirstLast.MatchString(string(v[len(v)-1])) {
121 return ErrLabelInvalidLastCharacter
122 }
123 // Body characters are a superset of the first character, and we've already
124 // checked that so we can check the entire string here.
125 if !reLabelBody.MatchString(v) {
126 return ErrLabelInvalidCharacter
127 }
128 return nil
129}
130
Serge Bazanski53458ba2024-06-18 09:56:46 +0000131// GetNodeLabel retrieves a node label by key, returning its value or an empty
132// string if no labels with this key is set on the node.
133func GetNodeLabel(labels *cpb.NodeLabels, key string) string {
134 for _, pair := range labels.Pairs {
135 if pair.Key == key {
136 return pair.Value
137 }
138 }
139 return ""
140}
Serge Bazanski6d1ff362024-09-30 15:15:31 +0000141
142// Labels on a node, a map from label key to value.
143type Labels map[string]string
144
145// Equals returns true if these Labels are equal to some others. Equality is
146// defined by having the same set of keys and corresponding values.
147func (l Labels) Equals(others Labels) bool {
148 for k, v := range l {
149 if v2, ok := others[k]; !ok || v != v2 {
150 return false
151 }
152 }
153 for k, v := range others {
154 if v2, ok := l[k]; !ok || v != v2 {
155 return false
156 }
157 }
158 return true
159}
160
161// Filter returns a subset of labels for which pred returns true.
162func (l Labels) Filter(pred func(k, v string) bool) Labels {
163 res := make(Labels)
164 for k, v := range l {
165 if pred(k, v) {
166 res[k] = v
167 }
168 }
169 return res
170}