k8s.io/kubernetes@v1.29.3/pkg/util/tolerations/tolerations_test.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 tolerations 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "math/rand" 23 "strings" 24 "testing" 25 26 "github.com/stretchr/testify/assert" 27 "github.com/stretchr/testify/require" 28 "k8s.io/apimachinery/pkg/util/validation/field" 29 api "k8s.io/kubernetes/pkg/apis/core" 30 "k8s.io/kubernetes/pkg/apis/core/validation" 31 utilpointer "k8s.io/utils/pointer" 32 ) 33 34 var ( 35 tolerations = map[string]api.Toleration{ 36 "all": {Operator: api.TolerationOpExists}, 37 "all-nosched": { 38 Operator: api.TolerationOpExists, 39 Effect: api.TaintEffectNoSchedule, 40 }, 41 "all-noexec": { 42 Operator: api.TolerationOpExists, 43 Effect: api.TaintEffectNoExecute, 44 }, 45 "foo": { 46 Key: "foo", 47 Operator: api.TolerationOpExists, 48 }, 49 "foo-bar": { 50 Key: "foo", 51 Operator: api.TolerationOpEqual, 52 Value: "bar", 53 }, 54 "foo-nosched": { 55 Key: "foo", 56 Operator: api.TolerationOpExists, 57 Effect: api.TaintEffectNoSchedule, 58 }, 59 "foo-bar-nosched": { 60 Key: "foo", 61 Operator: api.TolerationOpEqual, 62 Value: "bar", 63 Effect: api.TaintEffectNoSchedule, 64 }, 65 "foo-baz-nosched": { 66 Key: "foo", 67 Operator: api.TolerationOpEqual, 68 Value: "baz", 69 Effect: api.TaintEffectNoSchedule, 70 }, 71 "faz-nosched": { 72 Key: "faz", 73 Operator: api.TolerationOpExists, 74 Effect: api.TaintEffectNoSchedule, 75 }, 76 "faz-baz-nosched": { 77 Key: "faz", 78 Operator: api.TolerationOpEqual, 79 Value: "baz", 80 Effect: api.TaintEffectNoSchedule, 81 }, 82 "foo-prefnosched": { 83 Key: "foo", 84 Operator: api.TolerationOpExists, 85 Effect: api.TaintEffectPreferNoSchedule, 86 }, 87 "foo-noexec": { 88 Key: "foo", 89 Operator: api.TolerationOpExists, 90 Effect: api.TaintEffectNoExecute, 91 }, 92 "foo-bar-noexec": { 93 Key: "foo", 94 Operator: api.TolerationOpEqual, 95 Value: "bar", 96 Effect: api.TaintEffectNoExecute, 97 }, 98 "foo-noexec-10": { 99 Key: "foo", 100 Operator: api.TolerationOpExists, 101 Effect: api.TaintEffectNoExecute, 102 TolerationSeconds: utilpointer.Int64Ptr(10), 103 }, 104 "foo-noexec-0": { 105 Key: "foo", 106 Operator: api.TolerationOpExists, 107 Effect: api.TaintEffectNoExecute, 108 TolerationSeconds: utilpointer.Int64Ptr(0), 109 }, 110 "foo-bar-noexec-10": { 111 Key: "foo", 112 Operator: api.TolerationOpEqual, 113 Value: "bar", 114 Effect: api.TaintEffectNoExecute, 115 TolerationSeconds: utilpointer.Int64Ptr(10), 116 }, 117 } 118 ) 119 120 func TestIsSuperset(t *testing.T) { 121 tests := []struct { 122 toleration string 123 ss []string // t should be a superset of these 124 }{{ 125 "all", 126 []string{"all-nosched", "all-noexec", "foo", "foo-bar", "foo-nosched", "foo-bar-nosched", "foo-baz-nosched", "faz-nosched", "faz-baz-nosched", "foo-prefnosched", "foo-noexec", "foo-bar-noexec", "foo-noexec-10", "foo-noexec-0", "foo-bar-noexec-10"}, 127 }, { 128 "all-nosched", 129 []string{"foo-nosched", "foo-bar-nosched", "foo-baz-nosched", "faz-nosched", "faz-baz-nosched"}, 130 }, { 131 "all-noexec", 132 []string{"foo-noexec", "foo-bar-noexec", "foo-noexec-10", "foo-noexec-0", "foo-bar-noexec-10"}, 133 }, { 134 "foo", 135 []string{"foo-bar", "foo-nosched", "foo-bar-nosched", "foo-baz-nosched", "foo-prefnosched", "foo-noexec", "foo-bar-noexec", "foo-noexec-10", "foo-noexec-0", "foo-bar-noexec-10"}, 136 }, { 137 "foo-bar", 138 []string{"foo-bar-nosched", "foo-bar-noexec", "foo-bar-noexec-10"}, 139 }, { 140 "foo-nosched", 141 []string{"foo-bar-nosched", "foo-baz-nosched"}, 142 }, { 143 "foo-bar-nosched", 144 []string{}, 145 }, { 146 "faz-nosched", 147 []string{"faz-baz-nosched"}, 148 }, { 149 "faz-baz-nosched", 150 []string{}, 151 }, { 152 "foo-prenosched", 153 []string{}, 154 }, { 155 "foo-noexec", 156 []string{"foo-noexec", "foo-bar-noexec", "foo-noexec-10", "foo-noexec-0", "foo-bar-noexec-10"}, 157 }, { 158 "foo-bar-noexec", 159 []string{"foo-bar-noexec-10"}, 160 }, { 161 "foo-noexec-10", 162 []string{"foo-noexec-0", "foo-bar-noexec-10"}, 163 }, { 164 "foo-noexec-0", 165 []string{}, 166 }, { 167 "foo-bar-noexec-10", 168 []string{}, 169 }} 170 171 assertSuperset := func(t *testing.T, super, sub string) { 172 assert.True(t, isSuperset(tolerations[super], tolerations[sub]), 173 "%s should be a superset of %s", super, sub) 174 } 175 assertNotSuperset := func(t *testing.T, super, sub string) { 176 assert.False(t, isSuperset(tolerations[super], tolerations[sub]), 177 "%s should NOT be a superset of %s", super, sub) 178 } 179 contains := func(ss []string, s string) bool { 180 for _, str := range ss { 181 if str == s { 182 return true 183 } 184 } 185 return false 186 } 187 188 for _, test := range tests { 189 t.Run(test.toleration, func(t *testing.T) { 190 for name := range tolerations { 191 if name == test.toleration || contains(test.ss, name) { 192 assertSuperset(t, test.toleration, name) 193 } else { 194 assertNotSuperset(t, test.toleration, name) 195 } 196 } 197 }) 198 } 199 } 200 201 func TestVerifyAgainstWhitelist(t *testing.T) { 202 tests := []struct { 203 testName string 204 input []string 205 whitelist []string 206 expected bool 207 }{ 208 { 209 testName: "equal input and whitelist", 210 input: []string{"foo-bar-nosched", "foo-baz-nosched"}, 211 whitelist: []string{"foo-bar-nosched", "foo-baz-nosched"}, 212 expected: true, 213 }, 214 { 215 testName: "duplicate input allowed", 216 input: []string{"foo-bar-nosched", "foo-bar-nosched"}, 217 whitelist: []string{"foo-bar-nosched", "foo-baz-nosched"}, 218 expected: true, 219 }, 220 { 221 testName: "allow all", 222 input: []string{"foo-bar-nosched", "foo-bar-nosched"}, 223 whitelist: []string{"all"}, 224 expected: true, 225 }, 226 { 227 testName: "duplicate input forbidden", 228 input: []string{"foo-bar-nosched", "foo-bar-nosched"}, 229 whitelist: []string{"foo-baz-nosched"}, 230 expected: false, 231 }, 232 { 233 testName: "value mismatch", 234 input: []string{"foo-bar-nosched", "foo-baz-nosched"}, 235 whitelist: []string{"foo-baz-nosched"}, 236 expected: false, 237 }, 238 { 239 testName: "input does not exist in whitelist", 240 input: []string{"foo-bar-nosched"}, 241 whitelist: []string{"foo-baz-nosched"}, 242 expected: false, 243 }, 244 { 245 testName: "disjoint sets", 246 input: []string{"foo-bar"}, 247 whitelist: []string{"foo-nosched"}, 248 expected: false, 249 }, 250 { 251 testName: "empty whitelist", 252 input: []string{"foo-bar"}, 253 whitelist: []string{}, 254 expected: true, 255 }, 256 { 257 testName: "empty input", 258 input: []string{}, 259 whitelist: []string{"foo-bar"}, 260 expected: true, 261 }, 262 } 263 264 for _, c := range tests { 265 t.Run(c.testName, func(t *testing.T) { 266 actual := VerifyAgainstWhitelist(getTolerations(c.input), getTolerations(c.whitelist)) 267 assert.Equal(t, c.expected, actual) 268 }) 269 } 270 } 271 272 func TestMergeTolerations(t *testing.T) { 273 tests := []struct { 274 name string 275 a, b []string 276 expected []string 277 }{{ 278 name: "disjoint", 279 a: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10"}, 280 b: []string{"foo-prefnosched", "foo-baz-nosched"}, 281 expected: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10", "foo-prefnosched", "foo-baz-nosched"}, 282 }, { 283 name: "duplicate", 284 a: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10"}, 285 b: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10"}, 286 expected: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10"}, 287 }, { 288 name: "merge redundant", 289 a: []string{"foo-bar-nosched", "foo-baz-nosched"}, 290 b: []string{"foo-nosched", "faz-baz-nosched"}, 291 expected: []string{"foo-nosched", "faz-baz-nosched"}, 292 }, { 293 name: "merge all", 294 a: []string{"foo-bar-nosched", "foo-baz-nosched", "foo-noexec-10"}, 295 b: []string{"all"}, 296 expected: []string{"all"}, 297 }, { 298 name: "merge into all", 299 a: []string{"all"}, 300 b: []string{"foo-bar-nosched", "foo-baz-nosched", "foo-noexec-10"}, 301 expected: []string{"all"}, 302 }} 303 304 for _, test := range tests { 305 t.Run(test.name, func(t *testing.T) { 306 actual := MergeTolerations(getTolerations(test.a), getTolerations(test.b)) 307 require.Len(t, actual, len(test.expected)) 308 for i, expect := range getTolerations(test.expected) { 309 assert.Equal(t, expect, actual[i], "expected[%d] = %s", i, test.expected[i]) 310 } 311 }) 312 } 313 } 314 315 func TestFuzzed(t *testing.T) { 316 r := rand.New(rand.NewSource(1234)) // Fixed source to prevent flakes. 317 318 const ( 319 allProbability = 0.01 // Chance of getting a tolerate all 320 existsProbability = 0.3 321 tolerationSecondsProbability = 0.5 322 ) 323 effects := []api.TaintEffect{"", api.TaintEffectNoExecute, api.TaintEffectNoSchedule, api.TaintEffectPreferNoSchedule} 324 genToleration := func() api.Toleration { 325 gen := api.Toleration{ 326 Effect: effects[r.Intn(len(effects))], 327 } 328 if r.Float32() < allProbability { 329 gen = tolerations["all"] 330 return gen 331 } 332 // Small key/value space to encourage collisions 333 gen.Key = strings.Repeat("a", r.Intn(6)+1) 334 if r.Float32() < existsProbability { 335 gen.Operator = api.TolerationOpExists 336 } else { 337 gen.Operator = api.TolerationOpEqual 338 gen.Value = strings.Repeat("b", r.Intn(6)+1) 339 } 340 if gen.Effect == api.TaintEffectNoExecute && r.Float32() < tolerationSecondsProbability { 341 gen.TolerationSeconds = utilpointer.Int64Ptr(r.Int63n(10)) 342 } 343 // Ensure only valid tolerations are generated. 344 require.NoError(t, validation.ValidateTolerations([]api.Toleration{gen}, field.NewPath("")).ToAggregate(), "%#v", gen) 345 return gen 346 } 347 genTolerations := func() []api.Toleration { 348 result := []api.Toleration{} 349 for i := 0; i < r.Intn(10); i++ { 350 result = append(result, genToleration()) 351 } 352 return result 353 } 354 355 // Check whether the toleration is a subset of a toleration in the set. 356 isContained := func(toleration api.Toleration, set []api.Toleration) bool { 357 for _, ss := range set { 358 if isSuperset(ss, toleration) { 359 return true 360 } 361 } 362 return false 363 } 364 365 const iterations = 1000 366 367 debugMsg := func(tolerations ...[]api.Toleration) string { 368 str, err := json.Marshal(tolerations) 369 if err != nil { 370 return fmt.Sprintf("[ERR: %v] %v", err, tolerations) 371 } 372 return string(str) 373 } 374 t.Run("VerifyAgainstWhitelist", func(t *testing.T) { 375 for i := 0; i < iterations; i++ { 376 input := genTolerations() 377 whitelist := append(genTolerations(), genToleration()) // Non-empty 378 if VerifyAgainstWhitelist(input, whitelist) { 379 for _, tol := range input { 380 require.True(t, isContained(tol, whitelist), debugMsg(input, whitelist)) 381 } 382 } else { 383 uncontained := false 384 for _, tol := range input { 385 if !isContained(tol, whitelist) { 386 uncontained = true 387 break 388 } 389 } 390 require.True(t, uncontained, debugMsg(input, whitelist)) 391 } 392 } 393 }) 394 395 t.Run("MergeTolerations", func(t *testing.T) { 396 for i := 0; i < iterations; i++ { 397 a := genTolerations() 398 b := genTolerations() 399 result := MergeTolerations(a, b) 400 for _, tol := range append(a, b...) { 401 require.True(t, isContained(tol, result), debugMsg(a, b, result)) 402 } 403 } 404 }) 405 } 406 407 func getTolerations(names []string) []api.Toleration { 408 result := []api.Toleration{} 409 for _, name := range names { 410 result = append(result, tolerations[name]) 411 } 412 return result 413 }