Fix toleration comparison & merging logic

This commit is contained in:
Tim Allclair 2019-08-20 18:21:57 -07:00
parent 39724859b5
commit 5a50b3f4a2
4 changed files with 451 additions and 132 deletions

View File

@ -244,7 +244,6 @@ pkg/util/rlimit
pkg/util/selinux pkg/util/selinux
pkg/util/sysctl/testing pkg/util/sysctl/testing
pkg/util/taints pkg/util/taints
pkg/util/tolerations
pkg/version/verflag pkg/version/verflag
pkg/volume pkg/volume
pkg/volume/azure_dd pkg/volume/azure_dd

View File

@ -13,7 +13,11 @@ go_library(
"tolerations.go", "tolerations.go",
], ],
importpath = "k8s.io/kubernetes/pkg/util/tolerations", importpath = "k8s.io/kubernetes/pkg/util/tolerations",
deps = ["//pkg/apis/core:go_default_library"], deps = [
"//pkg/apis/core:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//vendor/k8s.io/klog:go_default_library",
],
) )
filegroup( filegroup(
@ -33,5 +37,12 @@ go_test(
name = "go_default_test", name = "go_default_test",
srcs = ["tolerations_test.go"], srcs = ["tolerations_test.go"],
embed = [":go_default_library"], embed = [":go_default_library"],
deps = ["//pkg/apis/core:go_default_library"], deps = [
"//pkg/apis/core:go_default_library",
"//pkg/apis/core/validation:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library",
"//vendor/k8s.io/utils/pointer:go_default_library",
],
) )

View File

@ -17,114 +17,91 @@ limitations under the License.
package tolerations package tolerations
import ( import (
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/klog"
api "k8s.io/kubernetes/pkg/apis/core" api "k8s.io/kubernetes/pkg/apis/core"
) )
type key struct {
tolerationKey string
effect api.TaintEffect
}
func convertTolerationToKey(in api.Toleration) key {
return key{in.Key, in.Effect}
}
// VerifyAgainstWhitelist checks if the provided tolerations // VerifyAgainstWhitelist checks if the provided tolerations
// satisfy the provided whitelist and returns true, otherwise returns false // satisfy the provided whitelist and returns true, otherwise returns false
func VerifyAgainstWhitelist(tolerations []api.Toleration, whitelist []api.Toleration) bool { func VerifyAgainstWhitelist(tolerations, whitelist []api.Toleration) bool {
if len(whitelist) == 0 { if len(whitelist) == 0 || len(tolerations) == 0 {
return true return true
} }
t := ConvertTolerationToAMap(tolerations) next:
w := ConvertTolerationToAMap(whitelist) for _, t := range tolerations {
for _, w := range whitelist {
for k1, v1 := range t { if isSuperset(w, t) {
if v2, ok := w[k1]; !ok || !AreEqual(v1, v2) { continue next
return false }
} }
}
return true
}
// IsConflict returns true if the key of two tolerations match
// but one or more other fields differ, otherwise returns false
func IsConflict(first []api.Toleration, second []api.Toleration) bool {
firstMap := ConvertTolerationToAMap(first)
secondMap := ConvertTolerationToAMap(second)
for k1, v1 := range firstMap {
if v2, ok := secondMap[k1]; ok && !AreEqual(v1, v2) {
return true
}
}
return false
}
// MergeTolerations merges two sets of tolerations into one
// it does not check for conflicts
func MergeTolerations(first []api.Toleration, second []api.Toleration) []api.Toleration {
var mergedTolerations []api.Toleration
mergedTolerations = append(mergedTolerations, second...)
firstMap := ConvertTolerationToAMap(first)
secondMap := ConvertTolerationToAMap(second)
// preserve order of first when merging
for _, v := range first {
k := convertTolerationToKey(v)
// if first contains key conflicts, the last one takes precedence
if _, ok := secondMap[k]; !ok && firstMap[k] == v {
mergedTolerations = append(mergedTolerations, v)
}
}
return mergedTolerations
}
// EqualTolerations returns true if two sets of tolerations are equal, otherwise false
// it assumes no duplicates in individual set of tolerations
func EqualTolerations(first []api.Toleration, second []api.Toleration) bool {
if len(first) != len(second) {
return false return false
} }
firstMap := ConvertTolerationToAMap(first)
secondMap := ConvertTolerationToAMap(second)
for k1, v1 := range firstMap {
if v2, ok := secondMap[k1]; !ok || !AreEqual(v1, v2) {
return false
}
}
return true return true
} }
// ConvertTolerationToAMap converts toleration list into a map[string]api.Toleration // MergeTolerations merges two sets of tolerations into one. If one toleration is a superset of
func ConvertTolerationToAMap(in []api.Toleration) map[key]api.Toleration { // another, only the superset is kept.
out := map[key]api.Toleration{} func MergeTolerations(first, second []api.Toleration) []api.Toleration {
for _, v := range in { all := append(first, second...)
out[convertTolerationToKey(v)] = v merged := []api.Toleration{}
next:
for i, t := range all {
for _, t2 := range merged {
if isSuperset(t2, t) {
continue next // t is redundant; ignore it
}
}
if i+1 < len(all) {
for _, t2 := range all[i+1:] {
// If the tolerations are equal, prefer the first.
if isSuperset(t2, t) && !apiequality.Semantic.DeepEqual(&t, &t2) {
continue next // t is redundant; ignore it
}
}
}
merged = append(merged, t)
} }
return out
return merged
} }
// AreEqual checks if two provided tolerations are equal or not. // isSuperset checks whether ss tolerates a superset of t.
func AreEqual(first, second api.Toleration) bool { func isSuperset(ss, t api.Toleration) bool {
if first.Key == second.Key && if apiequality.Semantic.DeepEqual(&t, &ss) {
first.Operator == second.Operator &&
first.Value == second.Value &&
first.Effect == second.Effect &&
AreTolerationSecondsEqual(first.TolerationSeconds, second.TolerationSeconds) {
return true return true
} }
return false
}
// AreTolerationSecondsEqual checks if two provided TolerationSeconds are equal or not. if t.Key != ss.Key &&
func AreTolerationSecondsEqual(ts1, ts2 *int64) bool { // An empty key with Exists operator means match all keys & values.
if ts1 == ts2 { (ss.Key != "" || ss.Operator != api.TolerationOpExists) {
return true return false
} }
if ts1 != nil && ts2 != nil && *ts1 == *ts2 {
return true // An empty effect means match all effects.
if t.Effect != ss.Effect && ss.Effect != "" {
return false
}
if ss.Effect == api.TaintEffectNoExecute {
if ss.TolerationSeconds != nil {
if t.TolerationSeconds == nil ||
*t.TolerationSeconds > *ss.TolerationSeconds {
return false
}
}
}
switch ss.Operator {
case api.TolerationOpEqual, "": // empty operator means Equal
return t.Operator == api.TolerationOpEqual && t.Value == ss.Value
case api.TolerationOpExists:
return true
default:
klog.Errorf("Unknown toleration operator: %s", ss.Operator)
return false
} }
return false
} }

View File

@ -17,65 +17,397 @@ limitations under the License.
package tolerations package tolerations
import ( import (
api "k8s.io/kubernetes/pkg/apis/core" "encoding/json"
"fmt"
"math/rand"
"strings"
"testing" "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) { func TestVerifyAgainstWhitelist(t *testing.T) {
tests := []struct { tests := []struct {
input []api.Toleration testName string
whitelist []api.Toleration input []string
testName string whitelist []string
testStatus bool expected bool
}{ }{
{ {
input: []api.Toleration{{Key: "foo", Operator: "Equal", Value: "bar", Effect: "NoSchedule"}}, testName: "equal input and whitelist",
whitelist: []api.Toleration{{Key: "foo", Operator: "Equal", Value: "bar", Effect: "NoSchedule"}}, input: []string{"foo-bar-nosched", "foo-baz-nosched"},
testName: "equal input and whitelist", whitelist: []string{"foo-bar-nosched", "foo-baz-nosched"},
testStatus: true, expected: true,
}, },
{ {
input: []api.Toleration{{Key: "foo", Operator: "Equal", Value: "bar", Effect: "NoSchedule"}}, testName: "duplicate input allowed",
whitelist: []api.Toleration{{Key: "foo", Operator: "Equal", Value: "bar", Effect: "NoExecute"}}, input: []string{"foo-bar-nosched", "foo-bar-nosched"},
testName: "input does not exist in whitelist", whitelist: []string{"foo-bar-nosched", "foo-baz-nosched"},
testStatus: false, 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 { for _, c := range tests {
status := VerifyAgainstWhitelist(c.input, c.whitelist) t.Run(c.testName, func(t *testing.T) {
if status != c.testStatus { actual := VerifyAgainstWhitelist(getTolerations(c.input), getTolerations(c.whitelist))
t.Errorf("Test: %s, expected %v", c.testName, status) assert.Equal(t, c.expected, actual)
} })
} }
} }
func TestIsConflict(t *testing.T) { func TestMergeTolerations(t *testing.T) {
tests := []struct { tests := []struct {
input1 []api.Toleration name string
input2 []api.Toleration a, b []string
testName string expected []string
testStatus bool }{{
}{ name: "disjoint",
{ a: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10"},
input1: []api.Toleration{{Key: "foo", Operator: "Equal", Value: "bar", Effect: "NoSchedule"}}, b: []string{"foo-prefnosched", "foo-baz-nosched"},
input2: []api.Toleration{{Key: "foo", Operator: "Equal", Value: "bar", Effect: "NoSchedule"}}, expected: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10", "foo-prefnosched", "foo-baz-nosched"},
testName: "equal inputs", }, {
testStatus: true, name: "duplicate",
}, a: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10"},
{ b: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10"},
input1: []api.Toleration{{Key: "foo", Operator: "Equal", Value: "foo", Effect: "NoExecute"}}, expected: []string{"foo-bar-nosched", "faz-baz-nosched", "foo-noexec-10"},
input2: []api.Toleration{{Key: "foo", Operator: "Equal", Value: "bar", Effect: "NoExecute"}}, }, {
testName: "mismatch values in inputs", name: "merge redundant",
testStatus: false, 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 _, c := range tests { for _, test := range tests {
status := IsConflict(c.input1, c.input2) t.Run(test.name, func(t *testing.T) {
if status == c.testStatus { actual := MergeTolerations(getTolerations(test.a), getTolerations(test.b))
t.Errorf("Test: %s, expected %v", c.testName, status) 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
}