| /* |
| Copyright 2017 The Kubernetes Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package tolerations |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "math/rand" |
| "strings" |
| "testing" |
| |
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
| "k8s.io/apimachinery/pkg/util/validation/field" |
| api "k8s.io/kubernetes/pkg/apis/core" |
| "k8s.io/kubernetes/pkg/apis/core/validation" |
| utilpointer "k8s.io/utils/pointer" |
| ) |
| |
| var ( |
| tolerations = map[string]api.Toleration{ |
| "all": {Operator: api.TolerationOpExists}, |
| "all-nosched": { |
| Operator: api.TolerationOpExists, |
| Effect: api.TaintEffectNoSchedule, |
| }, |
| "all-noexec": { |
| Operator: api.TolerationOpExists, |
| Effect: api.TaintEffectNoExecute, |
| }, |
| "foo": { |
| Key: "foo", |
| Operator: api.TolerationOpExists, |
| }, |
| "foo-bar": { |
| Key: "foo", |
| Operator: api.TolerationOpEqual, |
| Value: "bar", |
| }, |
| "foo-nosched": { |
| Key: "foo", |
| Operator: api.TolerationOpExists, |
| Effect: api.TaintEffectNoSchedule, |
| }, |
| "foo-bar-nosched": { |
| Key: "foo", |
| Operator: api.TolerationOpEqual, |
| Value: "bar", |
| Effect: api.TaintEffectNoSchedule, |
| }, |
| "foo-baz-nosched": { |
| Key: "foo", |
| Operator: api.TolerationOpEqual, |
| Value: "baz", |
| Effect: api.TaintEffectNoSchedule, |
| }, |
| "faz-nosched": { |
| Key: "faz", |
| Operator: api.TolerationOpExists, |
| Effect: api.TaintEffectNoSchedule, |
| }, |
| "faz-baz-nosched": { |
| Key: "faz", |
| Operator: api.TolerationOpEqual, |
| Value: "baz", |
| Effect: api.TaintEffectNoSchedule, |
| }, |
| "foo-prefnosched": { |
| Key: "foo", |
| Operator: api.TolerationOpExists, |
| Effect: api.TaintEffectPreferNoSchedule, |
| }, |
| "foo-noexec": { |
| Key: "foo", |
| Operator: api.TolerationOpExists, |
| Effect: api.TaintEffectNoExecute, |
| }, |
| "foo-bar-noexec": { |
| Key: "foo", |
| Operator: api.TolerationOpEqual, |
| Value: "bar", |
| Effect: api.TaintEffectNoExecute, |
| }, |
| "foo-noexec-10": { |
| Key: "foo", |
| Operator: api.TolerationOpExists, |
| Effect: api.TaintEffectNoExecute, |
| TolerationSeconds: utilpointer.Int64Ptr(10), |
| }, |
| "foo-noexec-0": { |
| Key: "foo", |
| Operator: api.TolerationOpExists, |
| Effect: api.TaintEffectNoExecute, |
| TolerationSeconds: utilpointer.Int64Ptr(0), |
| }, |
| "foo-bar-noexec-10": { |
| Key: "foo", |
| Operator: api.TolerationOpEqual, |
| Value: "bar", |
| Effect: api.TaintEffectNoExecute, |
| TolerationSeconds: utilpointer.Int64Ptr(10), |
| }, |
| } |
| ) |
| |
| func TestIsSuperset(t *testing.T) { |
| tests := []struct { |
| toleration string |
| ss []string // t should be a superset of these |
| }{{ |
| "all", |
| []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"}, |
| }, { |
| "all-nosched", |
| []string{"foo-nosched", "foo-bar-nosched", "foo-baz-nosched", "faz-nosched", "faz-baz-nosched"}, |
| }, { |
| "all-noexec", |
| []string{"foo-noexec", "foo-bar-noexec", "foo-noexec-10", "foo-noexec-0", "foo-bar-noexec-10"}, |
| }, { |
| "foo", |
| []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"}, |
| }, { |
| "foo-bar", |
| []string{"foo-bar-nosched", "foo-bar-noexec", "foo-bar-noexec-10"}, |
| }, { |
| "foo-nosched", |
| []string{"foo-bar-nosched", "foo-baz-nosched"}, |
| }, { |
| "foo-bar-nosched", |
| []string{}, |
| }, { |
| "faz-nosched", |
| []string{"faz-baz-nosched"}, |
| }, { |
| "faz-baz-nosched", |
| []string{}, |
| }, { |
| "foo-prenosched", |
| []string{}, |
| }, { |
| "foo-noexec", |
| []string{"foo-noexec", "foo-bar-noexec", "foo-noexec-10", "foo-noexec-0", "foo-bar-noexec-10"}, |
| }, { |
| "foo-bar-noexec", |
| []string{"foo-bar-noexec-10"}, |
| }, { |
| "foo-noexec-10", |
| []string{"foo-noexec-0", "foo-bar-noexec-10"}, |
| }, { |
| "foo-noexec-0", |
| []string{}, |
| }, { |
| "foo-bar-noexec-10", |
| []string{}, |
| }} |
| |
| assertSuperset := func(t *testing.T, super, sub string) { |
| assert.True(t, isSuperset(tolerations[super], tolerations[sub]), |
| "%s should be a superset of %s", super, sub) |
| } |
| assertNotSuperset := func(t *testing.T, super, sub string) { |
| assert.False(t, isSuperset(tolerations[super], tolerations[sub]), |
| "%s should NOT be a superset of %s", super, sub) |
| } |
| contains := func(ss []string, s string) bool { |
| for _, str := range ss { |
| if str == s { |
| return true |
| } |
| } |
| return false |
| } |
| |
| for _, test := range tests { |
| t.Run(test.toleration, func(t *testing.T) { |
| for name := range tolerations { |
| if name == test.toleration || contains(test.ss, name) { |
| assertSuperset(t, test.toleration, name) |
| } else { |
| assertNotSuperset(t, test.toleration, name) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestVerifyAgainstWhitelist(t *testing.T) { |
| tests := []struct { |
| testName string |
| input []string |
| whitelist []string |
| expected bool |
| }{ |
| { |
| testName: "equal input and whitelist", |
| input: []string{"foo-bar-nosched", "foo-baz-nosched"}, |
| whitelist: []string{"foo-bar-nosched", "foo-baz-nosched"}, |
| expected: true, |
| }, |
| { |
| testName: "duplicate input allowed", |
| input: []string{"foo-bar-nosched", "foo-bar-nosched"}, |
| whitelist: []string{"foo-bar-nosched", "foo-baz-nosched"}, |
| expected: true, |
| }, |
| { |
| testName: "allow all", |
| input: []string{"foo-bar-nosched", "foo-bar-nosched"}, |
| whitelist: []string{"all"}, |
| expected: true, |
| }, |
| { |
| testName: "duplicate input forbidden", |
| input: []string{"foo-bar-nosched", "foo-bar-nosched"}, |
| whitelist: []string{"foo-baz-nosched"}, |
| expected: false, |
| }, |
| { |
| testName: "value mismatch", |
| input: []string{"foo-bar-nosched", "foo-baz-nosched"}, |
| whitelist: []string{"foo-baz-nosched"}, |
| expected: false, |
| }, |
| { |
| testName: "input does not exist in whitelist", |
| input: []string{"foo-bar-nosched"}, |
| whitelist: []string{"foo-baz-nosched"}, |
| expected: false, |
| }, |
| { |
| testName: "disjoint sets", |
| input: []string{"foo-bar"}, |
| whitelist: []string{"foo-nosched"}, |
| expected: false, |
| }, |
| { |
| testName: "empty whitelist", |
| input: []string{"foo-bar"}, |
| whitelist: []string{}, |
| expected: true, |
| }, |
| { |
| testName: "empty input", |
| input: []string{}, |
| whitelist: []string{"foo-bar"}, |
| expected: true, |
| }, |
| } |
| |
| for _, c := range tests { |
| t.Run(c.testName, func(t *testing.T) { |
| actual := VerifyAgainstWhitelist(getTolerations(c.input), getTolerations(c.whitelist)) |
| assert.Equal(t, c.expected, actual) |
| }) |
| } |
| } |
| |
| func TestMergeTolerations(t *testing.T) { |
| tests := []struct { |
| name string |
| a, b []string |
| expected []string |
| }{{ |
| name: "disjoint", |
| a: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10"}, |
| b: []string{"foo-prefnosched", "foo-baz-nosched"}, |
| expected: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10", "foo-prefnosched", "foo-baz-nosched"}, |
| }, { |
| name: "duplicate", |
| a: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10"}, |
| b: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10"}, |
| expected: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10"}, |
| }, { |
| name: "merge redundant", |
| a: []string{"foo-bar-nosched", "foo-baz-nosched"}, |
| b: []string{"foo-nosched", "faz-baz-nosched"}, |
| expected: []string{"foo-nosched", "faz-baz-nosched"}, |
| }, { |
| name: "merge all", |
| a: []string{"foo-bar-nosched", "foo-baz-nosched", "foo-noexec-10"}, |
| b: []string{"all"}, |
| expected: []string{"all"}, |
| }, { |
| name: "merge into all", |
| a: []string{"all"}, |
| b: []string{"foo-bar-nosched", "foo-baz-nosched", "foo-noexec-10"}, |
| expected: []string{"all"}, |
| }} |
| |
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| actual := MergeTolerations(getTolerations(test.a), getTolerations(test.b)) |
| require.Len(t, actual, len(test.expected)) |
| for i, expect := range getTolerations(test.expected) { |
| assert.Equal(t, expect, actual[i], "expected[%d] = %s", i, test.expected[i]) |
| } |
| }) |
| } |
| } |
| |
| func TestFuzzed(t *testing.T) { |
| r := rand.New(rand.NewSource(1234)) // Fixed source to prevent flakes. |
| |
| const ( |
| allProbability = 0.01 // Chance of getting a tolerate all |
| existsProbability = 0.3 |
| tolerationSecondsProbability = 0.5 |
| ) |
| effects := []api.TaintEffect{"", api.TaintEffectNoExecute, api.TaintEffectNoSchedule, api.TaintEffectPreferNoSchedule} |
| genToleration := func() api.Toleration { |
| gen := api.Toleration{ |
| Effect: effects[r.Intn(len(effects))], |
| } |
| if r.Float32() < allProbability { |
| gen = tolerations["all"] |
| return gen |
| } |
| // Small key/value space to encourage collisions |
| gen.Key = strings.Repeat("a", r.Intn(6)+1) |
| if r.Float32() < existsProbability { |
| gen.Operator = api.TolerationOpExists |
| } else { |
| gen.Operator = api.TolerationOpEqual |
| gen.Value = strings.Repeat("b", r.Intn(6)+1) |
| } |
| if gen.Effect == api.TaintEffectNoExecute && r.Float32() < tolerationSecondsProbability { |
| gen.TolerationSeconds = utilpointer.Int64Ptr(r.Int63n(10)) |
| } |
| // Ensure only valid tolerations are generated. |
| require.NoError(t, validation.ValidateTolerations([]api.Toleration{gen}, field.NewPath("")).ToAggregate(), "%#v", gen) |
| return gen |
| } |
| genTolerations := func() []api.Toleration { |
| result := []api.Toleration{} |
| for i := 0; i < r.Intn(10); i++ { |
| result = append(result, genToleration()) |
| } |
| return result |
| } |
| |
| // Check whether the toleration is a subset of a toleration in the set. |
| isContained := func(toleration api.Toleration, set []api.Toleration) bool { |
| for _, ss := range set { |
| if isSuperset(ss, toleration) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| const iterations = 1000 |
| |
| debugMsg := func(tolerations ...[]api.Toleration) string { |
| str, err := json.Marshal(tolerations) |
| if err != nil { |
| return fmt.Sprintf("[ERR: %v] %v", err, tolerations) |
| } |
| return string(str) |
| } |
| t.Run("VerifyAgainstWhitelist", func(t *testing.T) { |
| for i := 0; i < iterations; i++ { |
| input := genTolerations() |
| whitelist := append(genTolerations(), genToleration()) // Non-empty |
| if VerifyAgainstWhitelist(input, whitelist) { |
| for _, tol := range input { |
| require.True(t, isContained(tol, whitelist), debugMsg(input, whitelist)) |
| } |
| } else { |
| uncontained := false |
| for _, tol := range input { |
| if !isContained(tol, whitelist) { |
| uncontained = true |
| break |
| } |
| } |
| require.True(t, uncontained, debugMsg(input, whitelist)) |
| } |
| } |
| }) |
| |
| t.Run("MergeTolerations", func(t *testing.T) { |
| for i := 0; i < iterations; i++ { |
| a := genTolerations() |
| b := genTolerations() |
| result := MergeTolerations(a, b) |
| for _, tol := range append(a, b...) { |
| require.True(t, isContained(tol, result), debugMsg(a, b, result)) |
| } |
| } |
| }) |
| } |
| |
| func getTolerations(names []string) []api.Toleration { |
| result := []api.Toleration{} |
| for _, name := range names { |
| result = append(result, tolerations[name]) |
| } |
| return result |
| } |