Skip to content

Commit 5a348c5

Browse files
helayotyk8s-publishing-bot
authored andcommitted
KEP-5471: Extend tolerations operators (#134665)
* Add numeric operations to tolerations Signed-off-by: Heba Elayoty <[email protected]> * code review feedback Signed-off-by: Heba Elayoty <[email protected]> * add default feature gate Signed-off-by: Heba Elayoty <[email protected]> * Add integration tests Signed-off-by: Heba Elayoty <[email protected]> * Add toleration value validation Signed-off-by: Heba Elayoty <[email protected]> * Add validate options for new operators Signed-off-by: helayoty <[email protected]> * Remove log Signed-off-by: helayoty <[email protected]> * Update feature gate check Signed-off-by: helayoty <[email protected]> * emove IsValidNumericString func Signed-off-by: helayoty <[email protected]> * Implement IsDecimalInteger Signed-off-by: helayoty <[email protected]> * code review feedback Signed-off-by: helayoty <[email protected]> * Add logs to v1/toleration Signed-off-by: Heba Elayoty <[email protected]> Signed-off-by: helayoty <[email protected]> * Update integration tests and address code review feedback Signed-off-by: helayoty <[email protected]> * Add feature gate to the scheduler framework Signed-off-by: helayoty <[email protected]> * Remove extra test Signed-off-by: helayoty <[email protected]> * Fix integration test Signed-off-by: helayoty <[email protected]> * pass feature gate via TolerationsTolerateTaint Signed-off-by: helayoty <[email protected]> --------- Signed-off-by: Heba Elayoty <[email protected]> Signed-off-by: helayoty <[email protected]> Kubernetes-commit: aceb89debc2632c5c9956c8b7ef591426a485447
1 parent 6f89492 commit 5a348c5

File tree

2 files changed

+296
-0
lines changed

2 files changed

+296
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
Copyright 2025 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 content
18+
19+
const decimalIntegerErrMsg string = "must be a valid decimal integer in canonical form"
20+
21+
// IsDecimalInteger validates that a string represents a decimal integer in strict canonical form.
22+
// This means the string must be formatted exactly as a human would naturally write an integer,
23+
// without any programming language conventions like leading zeros, plus signs, or alternate bases.
24+
//
25+
// valid values:"0" or Non-zero integers (i.e., "123", "-456") where the first digit is 1-9,
26+
// followed by any digits 0-9.
27+
//
28+
// This validator is stricter than strconv.ParseInt, which accepts leading zeros values (i.e, "0700")
29+
// and interprets them as decimal 700, potentially causing confusion with octal notation.
30+
func IsDecimalInteger(value string) []string {
31+
n := len(value)
32+
if n == 0 {
33+
return []string{EmptyError()}
34+
}
35+
36+
i := 0
37+
if value[0] == '-' {
38+
if n == 1 {
39+
return []string{decimalIntegerErrMsg}
40+
}
41+
i = 1
42+
}
43+
44+
if value[i] == '0' {
45+
if n == 1 && i == 0 {
46+
return nil
47+
}
48+
return []string{decimalIntegerErrMsg}
49+
}
50+
51+
if value[i] < '1' || value[i] > '9' {
52+
return []string{decimalIntegerErrMsg}
53+
}
54+
55+
for i++; i < n; i++ {
56+
if value[i] < '0' || value[i] > '9' {
57+
return []string{decimalIntegerErrMsg}
58+
}
59+
}
60+
61+
return nil
62+
}
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/*
2+
Copyright 2025 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 content
18+
19+
import (
20+
"strconv"
21+
"strings"
22+
"testing"
23+
)
24+
25+
func TestIsDecimalInteger(t *testing.T) {
26+
testCases := []struct {
27+
name string
28+
input string
29+
shouldPass bool
30+
errContains string
31+
}{
32+
// valid
33+
{name: "zero", input: "0", shouldPass: true},
34+
{name: "positive single digit 1", input: "1", shouldPass: true},
35+
{name: "positive single digit 2", input: "2", shouldPass: true},
36+
{name: "positive single digit 5", input: "5", shouldPass: true},
37+
{name: "positive single digit 9", input: "9", shouldPass: true},
38+
{name: "negative single digit", input: "-5", shouldPass: true},
39+
{name: "negative single digit 1", input: "-1", shouldPass: true},
40+
{name: "negative single digit 9", input: "-9", shouldPass: true},
41+
42+
{name: "number starting with 1", input: "100", shouldPass: true},
43+
{name: "number starting with 2", input: "234", shouldPass: true},
44+
{name: "number starting with 3", input: "345", shouldPass: true},
45+
{name: "number starting with 4", input: "456", shouldPass: true},
46+
{name: "number starting with 5", input: "567", shouldPass: true},
47+
{name: "number starting with 6", input: "678", shouldPass: true},
48+
{name: "number starting with 7", input: "789", shouldPass: true},
49+
{name: "number starting with 8", input: "890", shouldPass: true},
50+
{name: "number starting with 9", input: "999", shouldPass: true},
51+
52+
{name: "positive multi-digit", input: "123", shouldPass: true},
53+
{name: "negative multi-digit", input: "-456", shouldPass: true},
54+
{name: "negative starting with 1", input: "-100", shouldPass: true},
55+
{name: "negative starting with 9", input: "-987", shouldPass: true},
56+
{name: "large positive number", input: "9223372036854775807", shouldPass: true}, // max int64
57+
{name: "large negative number", input: "-9223372036854775808", shouldPass: true}, // min int64
58+
{name: "very long valid number", input: "12345678901234567890", shouldPass: true},
59+
{name: "all nines", input: "999999999999", shouldPass: true},
60+
61+
// invalid
62+
{name: "negative zero", input: "-0", shouldPass: false},
63+
{name: "double zero", input: "00", shouldPass: false},
64+
{name: "triple zero", input: "000", shouldPass: false},
65+
{name: "many zeros", input: "0000000", shouldPass: false},
66+
{name: "leading zero single digit", input: "01", shouldPass: false},
67+
{name: "leading zero digit 2", input: "02", shouldPass: false},
68+
{name: "leading zero digit 9", input: "09", shouldPass: false},
69+
{name: "leading zero multi-digit", input: "0123", shouldPass: false},
70+
{name: "octal-like format", input: "0700", shouldPass: false},
71+
{name: "octal-like format 2", input: "0950", shouldPass: false},
72+
{name: "multiple leading zeros", input: "00123", shouldPass: false},
73+
{name: "negative with leading zero", input: "-01", shouldPass: false},
74+
{name: "negative with leading zeros", input: "-0123", shouldPass: false},
75+
{name: "negative double zero", input: "-00", shouldPass: false},
76+
{name: "plus sign", input: "+123", shouldPass: false},
77+
{name: "positive plus sign", input: "+5", shouldPass: false},
78+
{name: "plus zero", input: "+0", shouldPass: false},
79+
80+
// Invalid cases - empty and whitespace
81+
{name: "empty string", input: "", shouldPass: false, errContains: "non-empty"},
82+
{name: "just minus sign", input: "-", shouldPass: false},
83+
{name: "just plus sign", input: "+", shouldPass: false},
84+
{name: "single space", input: " ", shouldPass: false},
85+
{name: "multiple spaces", input: " ", shouldPass: false},
86+
{name: "leading space", input: " 123", shouldPass: false},
87+
{name: "trailing space", input: "123 ", shouldPass: false},
88+
{name: "space in middle", input: "12 3", shouldPass: false},
89+
{name: "spaces around", input: " 123 ", shouldPass: false},
90+
91+
{name: "decimal number", input: "12.3", shouldPass: false},
92+
{name: "decimal zero", input: "0.0", shouldPass: false},
93+
{name: "negative decimal", input: "-12.5", shouldPass: false},
94+
{name: "trailing dot", input: "123.", shouldPass: false},
95+
{name: "leading dot", input: ".123", shouldPass: false},
96+
97+
{name: "alphabetic", input: "abc", shouldPass: false},
98+
{name: "alphanumeric", input: "12a3", shouldPass: false},
99+
{name: "letter at start", input: "a123", shouldPass: false},
100+
{name: "letter at end", input: "123a", shouldPass: false},
101+
{name: "uppercase letters", input: "ABC", shouldPass: false},
102+
{name: "mixed case", input: "12A3", shouldPass: false},
103+
104+
{name: "hexadecimal", input: "0x123", shouldPass: false},
105+
{name: "hex uppercase", input: "0X123", shouldPass: false},
106+
{name: "octal prefix", input: "0o777", shouldPass: false},
107+
{name: "binary prefix", input: "0b101", shouldPass: false},
108+
{name: "scientific notation", input: "1e5", shouldPass: false},
109+
{name: "scientific negative exp", input: "1e-5", shouldPass: false},
110+
{name: "scientific uppercase", input: "1E5", shouldPass: false},
111+
112+
{name: "underscore separator", input: "1_000", shouldPass: false},
113+
{name: "comma separator", input: "1,000", shouldPass: false},
114+
{name: "period separator", input: "1.000", shouldPass: false},
115+
{name: "apostrophe separator", input: "1'000", shouldPass: false},
116+
117+
{name: "double minus", input: "--123", shouldPass: false},
118+
{name: "double plus", input: "++123", shouldPass: false},
119+
{name: "plus minus", input: "+-123", shouldPass: false},
120+
{name: "minus plus", input: "-+123", shouldPass: false},
121+
{name: "minus at end", input: "123-", shouldPass: false},
122+
{name: "minus in middle", input: "12-3", shouldPass: false},
123+
{name: "plus at end", input: "123+", shouldPass: false},
124+
{name: "plus in middle", input: "12+3", shouldPass: false},
125+
126+
{name: "tab character at start", input: "\t123", shouldPass: false},
127+
{name: "tab character at end", input: "123\t", shouldPass: false},
128+
{name: "newline character", input: "123\n", shouldPass: false},
129+
{name: "carriage return", input: "123\r", shouldPass: false},
130+
{name: "null character", input: "123\x00", shouldPass: false},
131+
{name: "vertical tab", input: "123\v", shouldPass: false},
132+
{name: "form feed", input: "123\f", shouldPass: false},
133+
134+
{name: "parentheses", input: "(123)", shouldPass: false},
135+
{name: "brackets", input: "[123]", shouldPass: false},
136+
{name: "braces", input: "{123}", shouldPass: false},
137+
{name: "dollar sign", input: "$123", shouldPass: false},
138+
{name: "percent sign", input: "123%", shouldPass: false},
139+
{name: "hash", input: "#123", shouldPass: false},
140+
{name: "at sign", input: "@123", shouldPass: false},
141+
{name: "ampersand", input: "&123", shouldPass: false},
142+
{name: "asterisk", input: "*123", shouldPass: false},
143+
{name: "slash", input: "12/3", shouldPass: false},
144+
{name: "backslash", input: "12\\3", shouldPass: false},
145+
{name: "pipe", input: "12|3", shouldPass: false},
146+
{name: "semicolon", input: "12;3", shouldPass: false},
147+
{name: "colon", input: "12:3", shouldPass: false},
148+
{name: "question mark", input: "12?3", shouldPass: false},
149+
{name: "exclamation", input: "12!3", shouldPass: false},
150+
{name: "tilde", input: "~123", shouldPass: false},
151+
{name: "backtick", input: "`123", shouldPass: false},
152+
{name: "single quote", input: "'123'", shouldPass: false},
153+
{name: "double quote", input: "\"123\"", shouldPass: false},
154+
155+
{name: "unicode minus", input: "−123", shouldPass: false}, // U+2212 minus sign
156+
{name: "unicode digit", input: "123", shouldPass: false}, // fullwidth digits
157+
{name: "arabic digits", input: "١٢٣", shouldPass: false}, // Arabic-Indic digits
158+
{name: "chinese characters", input: "一二三", shouldPass: false},
159+
{name: "superscript", input: "123⁴", shouldPass: false},
160+
{name: "subscript", input: "123₄", shouldPass: false},
161+
}
162+
163+
for _, tc := range testCases {
164+
t.Run(tc.name, func(t *testing.T) {
165+
errs := IsDecimalInteger(tc.input)
166+
if tc.shouldPass {
167+
if len(errs) != 0 {
168+
t.Errorf("IsDecimalInteger(%q) = %v, want no errors", tc.input, errs)
169+
}
170+
} else {
171+
if len(errs) == 0 {
172+
t.Errorf("IsDecimalInteger(%q) = no errors, want errors", tc.input)
173+
} else if tc.errContains != "" {
174+
found := false
175+
for _, err := range errs {
176+
if strings.Contains(err, tc.errContains) {
177+
found = true
178+
break
179+
}
180+
}
181+
if !found {
182+
t.Errorf("IsDecimalInteger(%q) errors %v should contain %q", tc.input, errs, tc.errContains)
183+
}
184+
}
185+
}
186+
})
187+
}
188+
189+
// Additional verification: valid strings should parse with strconv.ParseInt
190+
validCases := []string{
191+
"0", "1", "2", "5", "9",
192+
"-1", "-5", "-9",
193+
"123", "-456", "100", "999",
194+
"9223372036854775807", "-9223372036854775808",
195+
"12345678901234567890",
196+
}
197+
for _, validCase := range validCases {
198+
if errs := IsDecimalInteger(validCase); len(errs) != 0 {
199+
t.Errorf("Valid case %q should return no errors, got: %v", validCase, errs)
200+
}
201+
// Verify it can also be parsed by strconv.ParseInt (within range)
202+
if len(validCase) <= 19 { // Only test cases that fit in int64
203+
if _, err := strconv.ParseInt(validCase, 10, 64); err != nil {
204+
t.Errorf("Valid case %q should be parseable by strconv.ParseInt: %v", validCase, err)
205+
}
206+
}
207+
}
208+
209+
// Verify that our function rejects what we intend to reject (even if strconv.ParseInt accepts it)
210+
rejectedCases := []string{
211+
"0700", "0950", "01", "02", "09",
212+
"+123", "+5", "+0",
213+
"-0", "00", "000",
214+
"-01", "-00",
215+
}
216+
for _, rejectedCase := range rejectedCases {
217+
if errs := IsDecimalInteger(rejectedCase); len(errs) == 0 {
218+
t.Errorf("Case %q should be rejected by strict validation", rejectedCase)
219+
}
220+
}
221+
222+
// Edge case: verify strconv.ParseInt accepts things we reject (proving we're stricter)
223+
strconvAcceptsButWeReject := []string{"+123", "0700", "01"}
224+
for _, case_ := range strconvAcceptsButWeReject {
225+
// strconv.ParseInt should accept it
226+
if _, err := strconv.ParseInt(case_, 10, 64); err != nil {
227+
t.Errorf("strconv.ParseInt should accept %q but got error: %v", case_, err)
228+
}
229+
// But our function should reject it
230+
if errs := IsDecimalInteger(case_); len(errs) == 0 {
231+
t.Errorf("IsDecimalInteger should reject %q (stricter than strconv)", case_)
232+
}
233+
}
234+
}

0 commit comments

Comments
 (0)