mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-26 13:07:07 +00:00
414 lines
11 KiB
Go
414 lines
11 KiB
Go
/*
|
|
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
|
|
}
|