mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-09-14 13:45:06 +00:00
DRA API: enhance validation testing
The line coverage is now at 98.5% and several more corner cases are covered. The remaining lines are hard or impossible to reach. The actual validation is the same as before, with some small tweaks to the generated errors. When failures are not as expected, it is useful to show what the expected and actual failures look like to a user. Perhaps even better would be to put the expected texts into the test files instead of the error structs. That would be easier to review and shorter.
This commit is contained in:
@@ -241,6 +241,9 @@ type QualifiedName string
|
|||||||
// FullyQualifiedName is a QualifiedName where the domain is set.
|
// FullyQualifiedName is a QualifiedName where the domain is set.
|
||||||
type FullyQualifiedName string
|
type FullyQualifiedName string
|
||||||
|
|
||||||
|
// DeviceMaxDomainLength is the maximum length of the domain prefix in a fully-qualified name.
|
||||||
|
const DeviceMaxDomainLength = 63
|
||||||
|
|
||||||
// DeviceMaxIDLength is the maximum length of the identifier in a device attribute or capacity name (`<domain>/<ID>`).
|
// DeviceMaxIDLength is the maximum length of the identifier in a device attribute or capacity name (`<domain>/<ID>`).
|
||||||
const DeviceMaxIDLength = 32
|
const DeviceMaxIDLength = 32
|
||||||
|
|
||||||
|
@@ -249,7 +249,7 @@ func validateOpaqueConfiguration(config resource.OpaqueDeviceConfiguration, fldP
|
|||||||
if len(config.Parameters.Raw) == 0 {
|
if len(config.Parameters.Raw) == 0 {
|
||||||
allErrs = append(allErrs, field.Required(fldPath.Child("parameters"), ""))
|
allErrs = append(allErrs, field.Required(fldPath.Child("parameters"), ""))
|
||||||
} else if err := json.Unmarshal(config.Parameters.Raw, &v); err != nil {
|
} else if err := json.Unmarshal(config.Parameters.Raw, &v); err != nil {
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("parameters"), "<value omitted>", fmt.Sprintf("error parsing data: %v", err.Error())))
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("parameters"), "<value omitted>", fmt.Sprintf("error parsing data as JSON: %v", err.Error())))
|
||||||
} else if v == nil {
|
} else if v == nil {
|
||||||
allErrs = append(allErrs, field.Required(fldPath.Child("parameters"), ""))
|
allErrs = append(allErrs, field.Required(fldPath.Child("parameters"), ""))
|
||||||
} else if _, isObject := v.(map[string]any); !isObject {
|
} else if _, isObject := v.(map[string]any); !isObject {
|
||||||
@@ -472,7 +472,7 @@ func validateResourceSliceSpec(spec, oldSpec *resource.ResourceSliceSpec, fldPat
|
|||||||
if len(spec.NodeSelector.NodeSelectorTerms) != 1 {
|
if len(spec.NodeSelector.NodeSelectorTerms) != 1 {
|
||||||
// This additional constraint simplifies merging of different selectors
|
// This additional constraint simplifies merging of different selectors
|
||||||
// when devices are allocated from different slices.
|
// when devices are allocated from different slices.
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("nodeSelector", "nodeSelectorTerms"), spec.NodeSelector.NodeSelectorTerms, "must have exactly one selector term"))
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("nodeSelector", "nodeSelectorTerms"), spec.NodeSelector.NodeSelectorTerms, "must have exactly one node selector term"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if spec.AllNodes {
|
if spec.AllNodes {
|
||||||
@@ -483,7 +483,7 @@ func validateResourceSliceSpec(spec, oldSpec *resource.ResourceSliceSpec, fldPat
|
|||||||
allErrs = append(allErrs, field.Required(fldPath, "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required"))
|
allErrs = append(allErrs, field.Required(fldPath, "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required"))
|
||||||
case 1:
|
case 1:
|
||||||
default:
|
default:
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath, spec, "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required"))
|
allErrs = append(allErrs, field.Invalid(fldPath, nil, "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
allErrs = append(allErrs, validateSet(spec.Devices, resource.ResourceSliceMaxDevices, validateDevice,
|
allErrs = append(allErrs, validateSet(spec.Devices, resource.ResourceSliceMaxDevices, validateDevice,
|
||||||
@@ -521,8 +521,9 @@ func validateBasicDevice(device resource.BasicDevice, fldPath *field.Path) field
|
|||||||
var allErrs field.ErrorList
|
var allErrs field.ErrorList
|
||||||
// Warn about exceeding the maximum length only once. If any individual
|
// Warn about exceeding the maximum length only once. If any individual
|
||||||
// field is too large, then so is the combination.
|
// field is too large, then so is the combination.
|
||||||
allErrs = append(allErrs, validateMap(device.Attributes, -1, validateQualifiedName, validateDeviceAttribute, fldPath.Child("attributes"))...)
|
maxKeyLen := resource.DeviceMaxDomainLength + 1 + resource.DeviceMaxIDLength
|
||||||
allErrs = append(allErrs, validateMap(device.Capacity, -1, validateQualifiedName, validateQuantity, fldPath.Child("capacity"))...)
|
allErrs = append(allErrs, validateMap(device.Attributes, -1, maxKeyLen, validateQualifiedName, validateDeviceAttribute, fldPath.Child("attributes"))...)
|
||||||
|
allErrs = append(allErrs, validateMap(device.Capacity, -1, maxKeyLen, validateQualifiedName, validateQuantity, fldPath.Child("capacity"))...)
|
||||||
if combinedLen, max := len(device.Attributes)+len(device.Capacity), resource.ResourceSliceMaxAttributesAndCapacitiesPerDevice; combinedLen > max {
|
if combinedLen, max := len(device.Attributes)+len(device.Capacity), resource.ResourceSliceMaxAttributesAndCapacitiesPerDevice; combinedLen > max {
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath, combinedLen, fmt.Sprintf("the total number of attributes and capacities must not exceed %d", max)))
|
allErrs = append(allErrs, field.Invalid(fldPath, combinedLen, fmt.Sprintf("the total number of attributes and capacities must not exceed %d", max)))
|
||||||
}
|
}
|
||||||
@@ -581,7 +582,7 @@ func validateDeviceAttribute(attribute resource.DeviceAttribute, fldPath *field.
|
|||||||
case 1:
|
case 1:
|
||||||
// Okay.
|
// Okay.
|
||||||
default:
|
default:
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath, attribute, "exactly one field must be specified"))
|
allErrs = append(allErrs, field.Invalid(fldPath, attribute, "exactly one value must be specified"))
|
||||||
}
|
}
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
@@ -604,7 +605,7 @@ func validateQualifiedName(name resource.QualifiedName, fldPath *field.Path) fie
|
|||||||
allErrs = append(allErrs, validateCIdentifier(parts[0], fldPath)...)
|
allErrs = append(allErrs, validateCIdentifier(parts[0], fldPath)...)
|
||||||
case 2:
|
case 2:
|
||||||
if len(parts[0]) == 0 {
|
if len(parts[0]) == 0 {
|
||||||
allErrs = append(allErrs, field.Required(fldPath, "the prefix must not be empty"))
|
allErrs = append(allErrs, field.Required(fldPath, "the domain must not be empty"))
|
||||||
} else {
|
} else {
|
||||||
allErrs = append(allErrs, validateDriverName(parts[0], fldPath)...)
|
allErrs = append(allErrs, validateDriverName(parts[0], fldPath)...)
|
||||||
}
|
}
|
||||||
@@ -619,8 +620,10 @@ func validateQualifiedName(name resource.QualifiedName, fldPath *field.Path) fie
|
|||||||
|
|
||||||
func validateFullyQualifiedName(name resource.FullyQualifiedName, fldPath *field.Path) field.ErrorList {
|
func validateFullyQualifiedName(name resource.FullyQualifiedName, fldPath *field.Path) field.ErrorList {
|
||||||
allErrs := validateQualifiedName(resource.QualifiedName(name), fldPath)
|
allErrs := validateQualifiedName(resource.QualifiedName(name), fldPath)
|
||||||
if !strings.Contains(string(name), "/") {
|
// validateQualifiedName checks that the name isn't empty and both parts are valid.
|
||||||
allErrs = append(allErrs, field.Required(fldPath.Child("domain"), "must include a prefix"))
|
// What we need to enforce here is that there really is a domain.
|
||||||
|
if name != "" && !strings.Contains(string(name), "/") {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath, name, "must include a domain"))
|
||||||
}
|
}
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
@@ -682,14 +685,34 @@ func stringKey(item string) (string, string) {
|
|||||||
|
|
||||||
// validateMap validates keys, items and the maximum length of a map.
|
// validateMap validates keys, items and the maximum length of a map.
|
||||||
// A negative maxSize disables the length check.
|
// A negative maxSize disables the length check.
|
||||||
func validateMap[K ~string, T any](m map[K]T, maxSize int, validateKey func(K, *field.Path) field.ErrorList, validateItem func(T, *field.Path) field.ErrorList, fldPath *field.Path) field.ErrorList {
|
//
|
||||||
|
// Keys larger than truncateKeyLen get truncated in the middle. A very
|
||||||
|
// small limit gets increased because it is okay to include more details.
|
||||||
|
// This is not used for validation of keys, which has to be done by
|
||||||
|
// the callback function.
|
||||||
|
func validateMap[K ~string, T any](m map[K]T, maxSize, truncateKeyLen int, validateKey func(K, *field.Path) field.ErrorList, validateItem func(T, *field.Path) field.ErrorList, fldPath *field.Path) field.ErrorList {
|
||||||
var allErrs field.ErrorList
|
var allErrs field.ErrorList
|
||||||
if maxSize >= 0 && len(m) > maxSize {
|
if maxSize >= 0 && len(m) > maxSize {
|
||||||
allErrs = append(allErrs, field.TooMany(fldPath, len(m), maxSize))
|
allErrs = append(allErrs, field.TooMany(fldPath, len(m), maxSize))
|
||||||
}
|
}
|
||||||
for key, item := range m {
|
for key, item := range m {
|
||||||
allErrs = append(allErrs, validateKey(key, fldPath)...)
|
keyPath := fldPath.Key(truncateIfTooLong(string(key), truncateKeyLen))
|
||||||
allErrs = append(allErrs, validateItem(item, fldPath.Key(string(key)))...)
|
allErrs = append(allErrs, validateKey(key, keyPath)...)
|
||||||
|
allErrs = append(allErrs, validateItem(item, keyPath)...)
|
||||||
}
|
}
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func truncateIfTooLong(str string, maxLen int) string {
|
||||||
|
// The caller was overly restrictive. Increase the length to something reasonable
|
||||||
|
// (https://github.com/kubernetes/kubernetes/pull/127511#discussion_r1826206362).
|
||||||
|
if maxLen < 16 {
|
||||||
|
maxLen = 16
|
||||||
|
}
|
||||||
|
if len(str) <= maxLen {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
ellipsis := "..."
|
||||||
|
remaining := maxLen - len(ellipsis)
|
||||||
|
return str[0:(remaining+1)/2] + ellipsis + str[len(str)-remaining/2:]
|
||||||
|
}
|
||||||
|
96
pkg/apis/resource/validation/validation_common_test.go
Normal file
96
pkg/apis/resource/validation/validation_common_test.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 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 validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
// assertFailures compares the expected against the actual errors.
|
||||||
|
//
|
||||||
|
// If they differ, it also logs what the formatted errors would look
|
||||||
|
// like to a user. This can be helpful to figure out whether an error
|
||||||
|
// is informative.
|
||||||
|
func assertFailures(tb testing.TB, want, got field.ErrorList) bool {
|
||||||
|
tb.Helper()
|
||||||
|
if !assert.Equal(tb, want, got) {
|
||||||
|
logFailures(tb, "Wanted failures", want)
|
||||||
|
logFailures(tb, "Got failures", got)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func logFailures(tb testing.TB, header string, errs field.ErrorList) {
|
||||||
|
tb.Helper()
|
||||||
|
tb.Logf("%s:\n", header)
|
||||||
|
for _, err := range errs {
|
||||||
|
tb.Logf("- %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncateIfTooLong(t *testing.T) {
|
||||||
|
for name, tc := range map[string]struct {
|
||||||
|
str string
|
||||||
|
maxLen int
|
||||||
|
expectStr string
|
||||||
|
}{
|
||||||
|
"nop": {
|
||||||
|
str: "hello",
|
||||||
|
maxLen: 10,
|
||||||
|
expectStr: "hello",
|
||||||
|
},
|
||||||
|
"truncate-to-limit": {
|
||||||
|
str: "hello world how are you",
|
||||||
|
maxLen: 18,
|
||||||
|
expectStr: "hello wo...are you",
|
||||||
|
},
|
||||||
|
"truncate-to-builtin-limit": {
|
||||||
|
str: "hello world how are you",
|
||||||
|
maxLen: 1, // Too small, gets increased.
|
||||||
|
expectStr: "hello w...re you",
|
||||||
|
},
|
||||||
|
"truncate-odd-string-even-limit": {
|
||||||
|
str: "abcdefghijklmnopqrs",
|
||||||
|
maxLen: 16,
|
||||||
|
expectStr: "abcdefg...nopqrs",
|
||||||
|
},
|
||||||
|
"truncate-even-string-even-limit": {
|
||||||
|
str: "abcdefghijklmnopqrst",
|
||||||
|
maxLen: 16,
|
||||||
|
expectStr: "abcdefg...opqrst",
|
||||||
|
},
|
||||||
|
"truncate-odd-string-odd-limit": {
|
||||||
|
str: "abcdefghijklmnopqrs",
|
||||||
|
maxLen: 17,
|
||||||
|
expectStr: "abcdefg...mnopqrs",
|
||||||
|
},
|
||||||
|
"truncate-even-string-odd-limit": {
|
||||||
|
str: "abcdefghijklmnopqrst",
|
||||||
|
maxLen: 17,
|
||||||
|
expectStr: "abcdefg...nopqrst",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.expectStr, truncateIfTooLong(tc.str, tc.maxLen))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -19,9 +19,8 @@ package validation
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource"
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
"k8s.io/utils/ptr"
|
"k8s.io/utils/ptr"
|
||||||
@@ -173,12 +172,139 @@ func TestValidateClass(t *testing.T) {
|
|||||||
return class
|
return class
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
|
"selectors": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.Required(field.NewPath("spec", "selectors").Index(1).Child("cel"), ""),
|
||||||
|
field.Invalid(field.NewPath("spec", "selectors").Index(2).Child("cel", "expression"), "noSuchVar", "compilation failed: ERROR: <input>:1:1: undeclared reference to 'noSuchVar' (in container '')\n | noSuchVar\n | ^"),
|
||||||
|
},
|
||||||
|
class: func() *resource.DeviceClass {
|
||||||
|
class := testClass(goodName)
|
||||||
|
validSelector := resource.DeviceSelector{
|
||||||
|
CEL: &resource.CELDeviceSelector{
|
||||||
|
Expression: "true",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
class.Spec.Selectors = []resource.DeviceSelector{
|
||||||
|
validSelector,
|
||||||
|
{
|
||||||
|
/* Missing CEL. */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CEL: &resource.CELDeviceSelector{
|
||||||
|
Expression: "noSuchVar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i := len(class.Spec.Selectors); i < resource.DeviceSelectorsMaxSize; i++ {
|
||||||
|
class.Spec.Selectors = append(class.Spec.Selectors, validSelector)
|
||||||
|
}
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"too-many-selectors": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.TooMany(field.NewPath("spec", "selectors"), resource.DeviceSelectorsMaxSize+1, resource.DeviceSelectorsMaxSize),
|
||||||
|
},
|
||||||
|
class: func() *resource.DeviceClass {
|
||||||
|
class := testClass(goodName)
|
||||||
|
validSelector := resource.DeviceSelector{
|
||||||
|
CEL: &resource.CELDeviceSelector{
|
||||||
|
Expression: "true",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i := 0; i < resource.DeviceSelectorsMaxSize+1; i++ {
|
||||||
|
class.Spec.Selectors = append(class.Spec.Selectors, validSelector)
|
||||||
|
}
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.Required(field.NewPath("spec", "config").Index(1).Child("opaque", "driver"), ""),
|
||||||
|
field.Invalid(field.NewPath("spec", "config").Index(1).Child("opaque", "driver"), "", "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||||
|
field.Required(field.NewPath("spec", "config").Index(1).Child("opaque", "parameters"), ""),
|
||||||
|
field.Invalid(field.NewPath("spec", "config").Index(2).Child("opaque", "parameters"), "<value omitted>", "error parsing data as JSON: invalid character 'x' looking for beginning of value"),
|
||||||
|
field.Invalid(field.NewPath("spec", "config").Index(3).Child("opaque", "parameters"), "<value omitted>", "parameters must be a valid JSON object"),
|
||||||
|
field.Required(field.NewPath("spec", "config").Index(4).Child("opaque", "parameters"), ""),
|
||||||
|
field.Required(field.NewPath("spec", "config").Index(5).Child("opaque"), ""),
|
||||||
|
},
|
||||||
|
class: func() *resource.DeviceClass {
|
||||||
|
class := testClass(goodName)
|
||||||
|
validConfig := resource.DeviceClassConfiguration{
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
|
Opaque: &resource.OpaqueDeviceConfiguration{
|
||||||
|
Driver: goodName,
|
||||||
|
Parameters: runtime.RawExtension{Raw: []byte(`{"foo":42}`)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
class.Spec.Config = []resource.DeviceClassConfiguration{
|
||||||
|
validConfig,
|
||||||
|
{
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
|
Opaque: &resource.OpaqueDeviceConfiguration{ /* Bad, both fields are required! */ },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
|
Opaque: &resource.OpaqueDeviceConfiguration{
|
||||||
|
Driver: goodName,
|
||||||
|
Parameters: runtime.RawExtension{Raw: []byte(`xxx`)}, /* Bad, not JSON. */
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
|
Opaque: &resource.OpaqueDeviceConfiguration{
|
||||||
|
Driver: goodName,
|
||||||
|
Parameters: runtime.RawExtension{Raw: []byte(`"hello-world"`)}, /* Bad, not object. */
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
|
Opaque: &resource.OpaqueDeviceConfiguration{
|
||||||
|
Driver: goodName,
|
||||||
|
Parameters: runtime.RawExtension{Raw: []byte(`null`)}, /* Bad, nil object. */
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{ /* Bad, empty. */ },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i := len(class.Spec.Config); i < resource.DeviceConfigMaxSize; i++ {
|
||||||
|
class.Spec.Config = append(class.Spec.Config, validConfig)
|
||||||
|
}
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"too-many-configs": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.TooMany(field.NewPath("spec", "config"), resource.DeviceConfigMaxSize+1, resource.DeviceConfigMaxSize),
|
||||||
|
},
|
||||||
|
class: func() *resource.DeviceClass {
|
||||||
|
class := testClass(goodName)
|
||||||
|
validConfig := resource.DeviceClassConfiguration{
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
|
Opaque: &resource.OpaqueDeviceConfiguration{
|
||||||
|
Driver: goodName,
|
||||||
|
Parameters: runtime.RawExtension{Raw: []byte(`{"foo":42}`)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i := len(class.Spec.Config); i < resource.DeviceConfigMaxSize+1; i++ {
|
||||||
|
class.Spec.Config = append(class.Spec.Config, validConfig)
|
||||||
|
}
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, scenario := range scenarios {
|
for name, scenario := range scenarios {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
errs := ValidateDeviceClass(scenario.class)
|
errs := ValidateDeviceClass(scenario.class)
|
||||||
assert.Equal(t, scenario.wantFailures, errs)
|
assertFailures(t, scenario.wantFailures, errs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +327,7 @@ func TestValidateClassUpdate(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
scenario.oldClass.ResourceVersion = "1"
|
scenario.oldClass.ResourceVersion = "1"
|
||||||
errs := ValidateDeviceClassUpdate(scenario.update(scenario.oldClass.DeepCopy()), scenario.oldClass)
|
errs := ValidateDeviceClassUpdate(scenario.update(scenario.oldClass.DeepCopy()), scenario.oldClass)
|
||||||
assert.Equal(t, scenario.wantFailures, errs)
|
assertFailures(t, scenario.wantFailures, errs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,8 +21,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
@@ -214,19 +212,127 @@ func TestValidateClaim(t *testing.T) {
|
|||||||
return claim
|
return claim
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
"invalid-request-name": {
|
"missing-classname": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "devices", "requests").Index(0).Child("deviceClassName"), "")},
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, validClaimSpec)
|
||||||
|
claim.Spec.Devices.Requests[0].DeviceClassName = ""
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"invalid-request": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.TooMany(field.NewPath("spec", "devices", "requests"), resource.DeviceRequestsMaxSize+1, resource.DeviceRequestsMaxSize),
|
||||||
|
field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("requests").Index(1), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"),
|
||||||
|
field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("requests").Index(1), badName, "must be the name of a request in the claim"),
|
||||||
|
field.TypeInvalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), "missing-domain", "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"),
|
||||||
|
field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), resource.FullyQualifiedName("missing-domain"), "must include a domain"),
|
||||||
|
field.Required(field.NewPath("spec", "devices", "constraints").Index(1).Child("matchAttribute"), "name required"),
|
||||||
|
field.Required(field.NewPath("spec", "devices", "constraints").Index(2).Child("matchAttribute"), ""),
|
||||||
|
field.TooMany(field.NewPath("spec", "devices", "constraints"), resource.DeviceConstraintsMaxSize+1, resource.DeviceConstraintsMaxSize),
|
||||||
|
field.Invalid(field.NewPath("spec", "devices", "config").Index(0).Child("requests").Index(1), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"),
|
||||||
|
field.Invalid(field.NewPath("spec", "devices", "config").Index(0).Child("requests").Index(1), badName, "must be the name of a request in the claim"),
|
||||||
|
field.TooMany(field.NewPath("spec", "devices", "config"), resource.DeviceConfigMaxSize+1, resource.DeviceConfigMaxSize),
|
||||||
|
},
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, validClaimSpec)
|
||||||
|
claim.Spec.Devices.Constraints = []resource.DeviceConstraint{
|
||||||
|
{
|
||||||
|
Requests: []string{claim.Spec.Devices.Requests[0].Name, badName},
|
||||||
|
MatchAttribute: ptr.To(resource.FullyQualifiedName("missing-domain")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MatchAttribute: ptr.To(resource.FullyQualifiedName("")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MatchAttribute: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i := len(claim.Spec.Devices.Constraints); i < resource.DeviceConstraintsMaxSize+1; i++ {
|
||||||
|
claim.Spec.Devices.Constraints = append(claim.Spec.Devices.Constraints, resource.DeviceConstraint{MatchAttribute: ptr.To(resource.FullyQualifiedName("foo/bar"))})
|
||||||
|
}
|
||||||
|
claim.Spec.Devices.Config = []resource.DeviceClaimConfiguration{{
|
||||||
|
Requests: []string{claim.Spec.Devices.Requests[0].Name, badName},
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
|
Opaque: &resource.OpaqueDeviceConfiguration{
|
||||||
|
Driver: "dra.example.com",
|
||||||
|
Parameters: runtime.RawExtension{
|
||||||
|
Raw: []byte(`{"kind": "foo", "apiVersion": "dra.example.com/v1"}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
for i := len(claim.Spec.Devices.Config); i < resource.DeviceConfigMaxSize+1; i++ {
|
||||||
|
claim.Spec.Devices.Config = append(claim.Spec.Devices.Config, resource.DeviceClaimConfiguration{
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
|
Opaque: &resource.OpaqueDeviceConfiguration{
|
||||||
|
Driver: "dra.example.com",
|
||||||
|
Parameters: runtime.RawExtension{
|
||||||
|
Raw: []byte(`{"kind": "foo", "apiVersion": "dra.example.com/v1"}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for i := len(claim.Spec.Devices.Requests); i < resource.DeviceRequestsMaxSize+1; i++ {
|
||||||
|
req := claim.Spec.Devices.Requests[0].DeepCopy()
|
||||||
|
req.Name += fmt.Sprintf("%d", i)
|
||||||
|
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *req)
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"valid-request": {
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, validClaimSpec)
|
||||||
|
for i := len(claim.Spec.Devices.Constraints); i < resource.DeviceConstraintsMaxSize; i++ {
|
||||||
|
claim.Spec.Devices.Constraints = append(claim.Spec.Devices.Constraints, resource.DeviceConstraint{MatchAttribute: ptr.To(resource.FullyQualifiedName("foo/bar"))})
|
||||||
|
}
|
||||||
|
for i := len(claim.Spec.Devices.Config); i < resource.DeviceConfigMaxSize; i++ {
|
||||||
|
claim.Spec.Devices.Config = append(claim.Spec.Devices.Config, resource.DeviceClaimConfiguration{
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
|
Opaque: &resource.OpaqueDeviceConfiguration{
|
||||||
|
Driver: "dra.example.com",
|
||||||
|
Parameters: runtime.RawExtension{
|
||||||
|
Raw: []byte(`{"kind": "foo", "apiVersion": "dra.example.com/v1"}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for i := len(claim.Spec.Devices.Requests); i < resource.DeviceRequestsMaxSize; i++ {
|
||||||
|
req := claim.Spec.Devices.Requests[0].DeepCopy()
|
||||||
|
req.Name += fmt.Sprintf("%d", i)
|
||||||
|
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *req)
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"invalid-spec": {
|
||||||
wantFailures: field.ErrorList{
|
wantFailures: field.ErrorList{
|
||||||
field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("requests").Index(1), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"),
|
field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("requests").Index(1), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"),
|
||||||
field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("requests").Index(1), badName, "must be the name of a request in the claim"),
|
field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("requests").Index(1), badName, "must be the name of a request in the claim"),
|
||||||
|
field.TypeInvalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), "missing-domain", "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"),
|
||||||
|
field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), resource.FullyQualifiedName("missing-domain"), "must include a domain"),
|
||||||
|
field.Required(field.NewPath("spec", "devices", "constraints").Index(1).Child("matchAttribute"), "name required"),
|
||||||
|
field.Required(field.NewPath("spec", "devices", "constraints").Index(2).Child("matchAttribute"), ""),
|
||||||
field.Invalid(field.NewPath("spec", "devices", "config").Index(0).Child("requests").Index(1), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"),
|
field.Invalid(field.NewPath("spec", "devices", "config").Index(0).Child("requests").Index(1), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"),
|
||||||
field.Invalid(field.NewPath("spec", "devices", "config").Index(0).Child("requests").Index(1), badName, "must be the name of a request in the claim"),
|
field.Invalid(field.NewPath("spec", "devices", "config").Index(0).Child("requests").Index(1), badName, "must be the name of a request in the claim"),
|
||||||
},
|
},
|
||||||
claim: func() *resource.ResourceClaim {
|
claim: func() *resource.ResourceClaim {
|
||||||
claim := testClaim(goodName, goodNS, validClaimSpec)
|
claim := testClaim(goodName, goodNS, validClaimSpec)
|
||||||
claim.Spec.Devices.Constraints = []resource.DeviceConstraint{{
|
claim.Spec.Devices.Constraints = []resource.DeviceConstraint{
|
||||||
|
{
|
||||||
Requests: []string{claim.Spec.Devices.Requests[0].Name, badName},
|
Requests: []string{claim.Spec.Devices.Requests[0].Name, badName},
|
||||||
MatchAttribute: ptr.To(resource.FullyQualifiedName("dra.example.com/numa")),
|
MatchAttribute: ptr.To(resource.FullyQualifiedName("missing-domain")),
|
||||||
}}
|
},
|
||||||
|
{
|
||||||
|
MatchAttribute: ptr.To(resource.FullyQualifiedName("")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MatchAttribute: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
claim.Spec.Devices.Config = []resource.DeviceClaimConfiguration{{
|
claim.Spec.Devices.Config = []resource.DeviceClaimConfiguration{{
|
||||||
Requests: []string{claim.Spec.Devices.Requests[0].Name, badName},
|
Requests: []string{claim.Spec.Devices.Requests[0].Name, badName},
|
||||||
DeviceConfiguration: resource.DeviceConfiguration{
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
@@ -241,10 +347,56 @@ func TestValidateClaim(t *testing.T) {
|
|||||||
return claim
|
return claim
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
|
"allocation-mode": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.Invalid(field.NewPath("spec", "devices", "requests").Index(2).Child("count"), int64(-1), "must be greater than zero"),
|
||||||
|
field.NotSupported(field.NewPath("spec", "devices", "requests").Index(3).Child("allocationMode"), resource.DeviceAllocationMode("other"), []resource.DeviceAllocationMode{resource.DeviceAllocationModeAll, resource.DeviceAllocationModeExactCount}),
|
||||||
|
field.Invalid(field.NewPath("spec", "devices", "requests").Index(4).Child("count"), int64(2), "must not be specified when allocationMode is 'All'"),
|
||||||
|
field.Duplicate(field.NewPath("spec", "devices", "requests").Index(5).Child("name"), "foo"),
|
||||||
|
},
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, validClaimSpec)
|
||||||
|
|
||||||
|
goodReq := &claim.Spec.Devices.Requests[0]
|
||||||
|
goodReq.Name = "foo"
|
||||||
|
goodReq.AllocationMode = resource.DeviceAllocationModeExactCount
|
||||||
|
goodReq.Count = 1
|
||||||
|
|
||||||
|
req := goodReq.DeepCopy()
|
||||||
|
req.Name += "2"
|
||||||
|
req.AllocationMode = resource.DeviceAllocationModeAll
|
||||||
|
req.Count = 0
|
||||||
|
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *req)
|
||||||
|
|
||||||
|
req = goodReq.DeepCopy()
|
||||||
|
req.Name += "3"
|
||||||
|
req.AllocationMode = resource.DeviceAllocationModeExactCount
|
||||||
|
req.Count = -1
|
||||||
|
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *req)
|
||||||
|
|
||||||
|
req = goodReq.DeepCopy()
|
||||||
|
req.Name += "4"
|
||||||
|
req.AllocationMode = resource.DeviceAllocationMode("other")
|
||||||
|
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *req)
|
||||||
|
|
||||||
|
req = goodReq.DeepCopy()
|
||||||
|
req.Name += "5"
|
||||||
|
req.AllocationMode = resource.DeviceAllocationModeAll
|
||||||
|
req.Count = 2
|
||||||
|
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *req)
|
||||||
|
|
||||||
|
req = goodReq.DeepCopy()
|
||||||
|
// Same name -> duplicate.
|
||||||
|
goodReq.Name = "foo"
|
||||||
|
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *req)
|
||||||
|
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
"invalid-config-json": {
|
"invalid-config-json": {
|
||||||
wantFailures: field.ErrorList{
|
wantFailures: field.ErrorList{
|
||||||
field.Required(field.NewPath("spec", "devices", "config").Index(0).Child("opaque", "parameters"), ""),
|
field.Required(field.NewPath("spec", "devices", "config").Index(0).Child("opaque", "parameters"), ""),
|
||||||
field.Invalid(field.NewPath("spec", "devices", "config").Index(1).Child("opaque", "parameters"), "<value omitted>", "error parsing data: unexpected end of JSON input"),
|
field.Invalid(field.NewPath("spec", "devices", "config").Index(1).Child("opaque", "parameters"), "<value omitted>", "error parsing data as JSON: unexpected end of JSON input"),
|
||||||
field.Invalid(field.NewPath("spec", "devices", "config").Index(2).Child("opaque", "parameters"), "<value omitted>", "parameters must be a valid JSON object"),
|
field.Invalid(field.NewPath("spec", "devices", "config").Index(2).Child("opaque", "parameters"), "<value omitted>", "parameters must be a valid JSON object"),
|
||||||
field.Required(field.NewPath("spec", "devices", "config").Index(3).Child("opaque", "parameters"), ""),
|
field.Required(field.NewPath("spec", "devices", "config").Index(3).Child("opaque", "parameters"), ""),
|
||||||
},
|
},
|
||||||
@@ -367,7 +519,7 @@ func TestValidateClaim(t *testing.T) {
|
|||||||
for name, scenario := range scenarios {
|
for name, scenario := range scenarios {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
errs := ValidateResourceClaim(scenario.claim)
|
errs := ValidateResourceClaim(scenario.claim)
|
||||||
assert.Equal(t, scenario.wantFailures, errs)
|
assertFailures(t, scenario.wantFailures, errs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,7 +552,7 @@ func TestValidateClaimUpdate(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
scenario.oldClaim.ResourceVersion = "1"
|
scenario.oldClaim.ResourceVersion = "1"
|
||||||
errs := ValidateResourceClaimUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim)
|
errs := ValidateResourceClaimUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim)
|
||||||
assert.Equal(t, scenario.wantFailures, errs)
|
assertFailures(t, scenario.wantFailures, errs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -689,6 +841,68 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
|
|||||||
return claim
|
return claim
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"configuration": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.Required(field.NewPath("status", "allocation", "devices", "config").Index(1).Child("source"), ""),
|
||||||
|
field.NotSupported(field.NewPath("status", "allocation", "devices", "config").Index(2).Child("source"), resource.AllocationConfigSource("no-such-source"), []resource.AllocationConfigSource{resource.AllocationConfigSourceClaim, resource.AllocationConfigSourceClass}),
|
||||||
|
field.Required(field.NewPath("status", "allocation", "devices", "config").Index(3).Child("opaque"), ""),
|
||||||
|
field.Required(field.NewPath("status", "allocation", "devices", "config").Index(4).Child("opaque", "driver"), ""),
|
||||||
|
field.Invalid(field.NewPath("status", "allocation", "devices", "config").Index(4).Child("opaque", "driver"), "", "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||||
|
field.Required(field.NewPath("status", "allocation", "devices", "config").Index(4).Child("opaque", "parameters"), ""),
|
||||||
|
},
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim = claim.DeepCopy()
|
||||||
|
claim.Status.Allocation = validAllocatedClaim.Status.Allocation.DeepCopy()
|
||||||
|
claim.Status.Allocation.Devices.Config = []resource.DeviceAllocationConfiguration{
|
||||||
|
{
|
||||||
|
Source: resource.AllocationConfigSourceClaim,
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
|
Opaque: &resource.OpaqueDeviceConfiguration{
|
||||||
|
Driver: "dra.example.com",
|
||||||
|
Parameters: runtime.RawExtension{
|
||||||
|
Raw: []byte(`{"kind": "foo", "apiVersion": "dra.example.com/v1"}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Source: "", /* Empty! */
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
|
Opaque: &resource.OpaqueDeviceConfiguration{
|
||||||
|
Driver: "dra.example.com",
|
||||||
|
Parameters: runtime.RawExtension{
|
||||||
|
Raw: []byte(`{"kind": "foo", "apiVersion": "dra.example.com/v1"}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Source: resource.AllocationConfigSource("no-such-source"),
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
|
Opaque: &resource.OpaqueDeviceConfiguration{
|
||||||
|
Driver: "dra.example.com",
|
||||||
|
Parameters: runtime.RawExtension{
|
||||||
|
Raw: []byte(`{"kind": "foo", "apiVersion": "dra.example.com/v1"}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Source: resource.AllocationConfigSourceClaim,
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{ /* Empty! */ },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Source: resource.AllocationConfigSourceClaim,
|
||||||
|
DeviceConfiguration: resource.DeviceConfiguration{
|
||||||
|
Opaque: &resource.OpaqueDeviceConfiguration{ /* Empty! */ },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Other invalid resource.DeviceConfiguration are covered elsewhere. */
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, scenario := range scenarios {
|
for name, scenario := range scenarios {
|
||||||
@@ -696,7 +910,7 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
|
|||||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAAdminAccess, scenario.adminAccess)
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAAdminAccess, scenario.adminAccess)
|
||||||
scenario.oldClaim.ResourceVersion = "1"
|
scenario.oldClaim.ResourceVersion = "1"
|
||||||
errs := ValidateResourceClaimStatusUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim)
|
errs := ValidateResourceClaimStatusUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim)
|
||||||
assert.Equal(t, scenario.wantFailures, errs)
|
assertFailures(t, scenario.wantFailures, errs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,8 +19,6 @@ package validation
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource"
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
@@ -192,7 +190,7 @@ func TestValidateClaimTemplate(t *testing.T) {
|
|||||||
for name, scenario := range scenarios {
|
for name, scenario := range scenarios {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
errs := ValidateResourceClaimTemplate(scenario.template)
|
errs := ValidateResourceClaimTemplate(scenario.template)
|
||||||
assert.Equal(t, scenario.wantFailures, errs)
|
assertFailures(t, scenario.wantFailures, errs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,7 +225,7 @@ func TestValidateClaimTemplateUpdate(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
scenario.oldClaimTemplate.ResourceVersion = "1"
|
scenario.oldClaimTemplate.ResourceVersion = "1"
|
||||||
errs := ValidateResourceClaimTemplateUpdate(scenario.update(scenario.oldClaimTemplate.DeepCopy()), scenario.oldClaimTemplate)
|
errs := ValidateResourceClaimTemplateUpdate(scenario.update(scenario.oldClaimTemplate.DeepCopy()), scenario.oldClaimTemplate)
|
||||||
assert.Equal(t, scenario.wantFailures, errs)
|
assertFailures(t, scenario.wantFailures, errs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,30 +17,58 @@ limitations under the License.
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource"
|
"k8s.io/kubernetes/pkg/apis/core"
|
||||||
|
resourceapi "k8s.io/kubernetes/pkg/apis/resource"
|
||||||
"k8s.io/utils/ptr"
|
"k8s.io/utils/ptr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testResourceSlice(name, nodeName, driverName string) *resource.ResourceSlice {
|
func testAttributes() map[resourceapi.QualifiedName]resourceapi.DeviceAttribute {
|
||||||
return &resource.ResourceSlice{
|
return map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
|
||||||
|
"int": {IntValue: ptr.To(int64(42))},
|
||||||
|
"string": {StringValue: ptr.To("hello world")},
|
||||||
|
"version": {VersionValue: ptr.To("1.2.3")},
|
||||||
|
"bool": {BoolValue: ptr.To(true)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCapacity() map[resourceapi.QualifiedName]resource.Quantity {
|
||||||
|
return map[resourceapi.QualifiedName]resource.Quantity{
|
||||||
|
"memory": resource.MustParse("1Gi"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testResourceSlice(name, nodeName, driverName string, numDevices int) *resourceapi.ResourceSlice {
|
||||||
|
slice := &resourceapi.ResourceSlice{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: name,
|
Name: name,
|
||||||
},
|
},
|
||||||
Spec: resource.ResourceSliceSpec{
|
Spec: resourceapi.ResourceSliceSpec{
|
||||||
NodeName: nodeName,
|
NodeName: nodeName,
|
||||||
Driver: driverName,
|
Driver: driverName,
|
||||||
Pool: resource.ResourcePool{
|
Pool: resourceapi.ResourcePool{
|
||||||
Name: nodeName,
|
Name: nodeName,
|
||||||
ResourceSliceCount: 1,
|
ResourceSliceCount: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
for i := 0; i < numDevices; i++ {
|
||||||
|
device := resourceapi.Device{
|
||||||
|
Name: fmt.Sprintf("device-%d", i),
|
||||||
|
Basic: &resourceapi.BasicDevice{
|
||||||
|
Attributes: testAttributes(),
|
||||||
|
Capacity: testCapacity(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
slice.Spec.Devices = append(slice.Spec.Devices, device)
|
||||||
|
}
|
||||||
|
return slice
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateResourceSlice(t *testing.T) {
|
func TestValidateResourceSlice(t *testing.T) {
|
||||||
@@ -51,65 +79,69 @@ func TestValidateResourceSlice(t *testing.T) {
|
|||||||
badValue := "spaces not allowed"
|
badValue := "spaces not allowed"
|
||||||
|
|
||||||
scenarios := map[string]struct {
|
scenarios := map[string]struct {
|
||||||
slice *resource.ResourceSlice
|
slice *resourceapi.ResourceSlice
|
||||||
wantFailures field.ErrorList
|
wantFailures field.ErrorList
|
||||||
}{
|
}{
|
||||||
"good": {
|
"good": {
|
||||||
slice: testResourceSlice(goodName, goodName, driverName),
|
slice: testResourceSlice(goodName, goodName, driverName, resourceapi.ResourceSliceMaxDevices),
|
||||||
|
},
|
||||||
|
"too-large": {
|
||||||
|
wantFailures: field.ErrorList{field.TooMany(field.NewPath("spec", "devices"), resourceapi.ResourceSliceMaxDevices+1, resourceapi.ResourceSliceMaxDevices)},
|
||||||
|
slice: testResourceSlice(goodName, goodName, goodName, resourceapi.ResourceSliceMaxDevices+1),
|
||||||
},
|
},
|
||||||
"missing-name": {
|
"missing-name": {
|
||||||
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
|
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
|
||||||
slice: testResourceSlice("", goodName, driverName),
|
slice: testResourceSlice("", goodName, driverName, 1),
|
||||||
},
|
},
|
||||||
"bad-name": {
|
"bad-name": {
|
||||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||||
slice: testResourceSlice(badName, goodName, driverName),
|
slice: testResourceSlice(badName, goodName, driverName, 1),
|
||||||
},
|
},
|
||||||
"generate-name": {
|
"generate-name": {
|
||||||
slice: func() *resource.ResourceSlice {
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
slice := testResourceSlice(goodName, goodName, driverName)
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
slice.GenerateName = "prefix-"
|
slice.GenerateName = "prefix-"
|
||||||
return slice
|
return slice
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
"uid": {
|
"uid": {
|
||||||
slice: func() *resource.ResourceSlice {
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
slice := testResourceSlice(goodName, goodName, driverName)
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
slice.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
|
slice.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
|
||||||
return slice
|
return slice
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
"resource-version": {
|
"resource-version": {
|
||||||
slice: func() *resource.ResourceSlice {
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
slice := testResourceSlice(goodName, goodName, driverName)
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
slice.ResourceVersion = "1"
|
slice.ResourceVersion = "1"
|
||||||
return slice
|
return slice
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
"generation": {
|
"generation": {
|
||||||
slice: func() *resource.ResourceSlice {
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
slice := testResourceSlice(goodName, goodName, driverName)
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
slice.Generation = 100
|
slice.Generation = 100
|
||||||
return slice
|
return slice
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
"creation-timestamp": {
|
"creation-timestamp": {
|
||||||
slice: func() *resource.ResourceSlice {
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
slice := testResourceSlice(goodName, goodName, driverName)
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
slice.CreationTimestamp = now
|
slice.CreationTimestamp = now
|
||||||
return slice
|
return slice
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
"deletion-grace-period-seconds": {
|
"deletion-grace-period-seconds": {
|
||||||
slice: func() *resource.ResourceSlice {
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
slice := testResourceSlice(goodName, goodName, driverName)
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
slice.DeletionGracePeriodSeconds = ptr.To[int64](10)
|
slice.DeletionGracePeriodSeconds = ptr.To[int64](10)
|
||||||
return slice
|
return slice
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
"owner-references": {
|
"owner-references": {
|
||||||
slice: func() *resource.ResourceSlice {
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
slice := testResourceSlice(goodName, goodName, driverName)
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
slice.OwnerReferences = []metav1.OwnerReference{
|
slice.OwnerReferences = []metav1.OwnerReference{
|
||||||
{
|
{
|
||||||
APIVersion: "v1",
|
APIVersion: "v1",
|
||||||
@@ -122,8 +154,8 @@ func TestValidateResourceSlice(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
"finalizers": {
|
"finalizers": {
|
||||||
slice: func() *resource.ResourceSlice {
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
slice := testResourceSlice(goodName, goodName, driverName)
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
slice.Finalizers = []string{
|
slice.Finalizers = []string{
|
||||||
"example.com/foo",
|
"example.com/foo",
|
||||||
}
|
}
|
||||||
@@ -131,8 +163,8 @@ func TestValidateResourceSlice(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
"managed-fields": {
|
"managed-fields": {
|
||||||
slice: func() *resource.ResourceSlice {
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
slice := testResourceSlice(goodName, goodName, driverName)
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
slice.ManagedFields = []metav1.ManagedFieldsEntry{
|
slice.ManagedFields = []metav1.ManagedFieldsEntry{
|
||||||
{
|
{
|
||||||
FieldsType: "FieldsV1",
|
FieldsType: "FieldsV1",
|
||||||
@@ -145,8 +177,8 @@ func TestValidateResourceSlice(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
"good-labels": {
|
"good-labels": {
|
||||||
slice: func() *resource.ResourceSlice {
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
slice := testResourceSlice(goodName, goodName, driverName)
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
slice.Labels = map[string]string{
|
slice.Labels = map[string]string{
|
||||||
"apps.kubernetes.io/name": "test",
|
"apps.kubernetes.io/name": "test",
|
||||||
}
|
}
|
||||||
@@ -155,8 +187,8 @@ func TestValidateResourceSlice(t *testing.T) {
|
|||||||
},
|
},
|
||||||
"bad-labels": {
|
"bad-labels": {
|
||||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
|
||||||
slice: func() *resource.ResourceSlice {
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
slice := testResourceSlice(goodName, goodName, driverName)
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
slice.Labels = map[string]string{
|
slice.Labels = map[string]string{
|
||||||
"hello-world": badValue,
|
"hello-world": badValue,
|
||||||
}
|
}
|
||||||
@@ -164,8 +196,8 @@ func TestValidateResourceSlice(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
"good-annotations": {
|
"good-annotations": {
|
||||||
slice: func() *resource.ResourceSlice {
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
slice := testResourceSlice(goodName, goodName, driverName)
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
slice.Annotations = map[string]string{
|
slice.Annotations = map[string]string{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
}
|
}
|
||||||
@@ -174,8 +206,8 @@ func TestValidateResourceSlice(t *testing.T) {
|
|||||||
},
|
},
|
||||||
"bad-annotations": {
|
"bad-annotations": {
|
||||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
|
||||||
slice: func() *resource.ResourceSlice {
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
slice := testResourceSlice(goodName, goodName, driverName)
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
slice.Annotations = map[string]string{
|
slice.Annotations = map[string]string{
|
||||||
badName: "hello world",
|
badName: "hello world",
|
||||||
}
|
}
|
||||||
@@ -187,7 +219,7 @@ func TestValidateResourceSlice(t *testing.T) {
|
|||||||
field.Invalid(field.NewPath("spec", "pool", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
field.Invalid(field.NewPath("spec", "pool", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||||
field.Invalid(field.NewPath("spec", "nodeName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
field.Invalid(field.NewPath("spec", "nodeName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||||
},
|
},
|
||||||
slice: testResourceSlice(goodName, badName, driverName),
|
slice: testResourceSlice(goodName, badName, driverName, 1),
|
||||||
},
|
},
|
||||||
"bad-multi-pool-name": {
|
"bad-multi-pool-name": {
|
||||||
wantFailures: field.ErrorList{
|
wantFailures: field.ErrorList{
|
||||||
@@ -195,38 +227,234 @@ func TestValidateResourceSlice(t *testing.T) {
|
|||||||
field.Invalid(field.NewPath("spec", "pool", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
field.Invalid(field.NewPath("spec", "pool", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||||
field.Invalid(field.NewPath("spec", "nodeName"), badName+"/"+badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
field.Invalid(field.NewPath("spec", "nodeName"), badName+"/"+badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||||
},
|
},
|
||||||
slice: testResourceSlice(goodName, badName+"/"+badName, driverName),
|
slice: testResourceSlice(goodName, badName+"/"+badName, driverName, 1),
|
||||||
|
},
|
||||||
|
"good-pool-name": {
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
|
slice.Spec.Pool.Name = strings.Repeat("x", resourceapi.PoolNameMaxLength)
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-pool": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.TooLongMaxLength(field.NewPath("spec", "pool", "name"), strings.Repeat("x/", resourceapi.PoolNameMaxLength/2)+"xy", resourceapi.PoolNameMaxLength),
|
||||||
|
field.Invalid(field.NewPath("spec", "pool", "resourceSliceCount"), int64(0), "must be greater than zero"),
|
||||||
|
field.Invalid(field.NewPath("spec", "pool", "generation"), int64(-1), "must be greater than or equal to zero"),
|
||||||
|
},
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
|
slice.Spec.Pool.Name = strings.Repeat("x/", resourceapi.PoolNameMaxLength/2) + "xy"
|
||||||
|
slice.Spec.Pool.ResourceSliceCount = 0
|
||||||
|
slice.Spec.Pool.Generation = -1
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"missing-pool-name": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.Required(field.NewPath("spec", "pool", "name"), ""),
|
||||||
|
},
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
|
slice.Spec.Pool.Name = ""
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-empty-node-selector": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.Required(field.NewPath("spec", "nodeSelector", "nodeSelectorTerms"), "must have at least one node selector term"), // From core validation.
|
||||||
|
field.Invalid(field.NewPath("spec", "nodeSelector", "nodeSelectorTerms"), []core.NodeSelectorTerm(nil), "must have exactly one node selector term"), // From DRA validation.
|
||||||
|
},
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
|
slice.Spec.NodeName = ""
|
||||||
|
slice.Spec.NodeSelector = &core.NodeSelector{}
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-node-selection": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), nil, "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required")},
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
|
slice.Spec.NodeName = "worker"
|
||||||
|
slice.Spec.NodeSelector = &core.NodeSelector{
|
||||||
|
NodeSelectorTerms: []core.NodeSelectorTerm{{MatchFields: []core.NodeSelectorRequirement{{Key: "metadata.name", Operator: core.NodeSelectorOpIn, Values: []string{"worker"}}}}},
|
||||||
|
}
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-node-selection-all-nodes": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), nil, "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required")},
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
|
slice.Spec.NodeName = "worker"
|
||||||
|
slice.Spec.AllNodes = true
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"empty-node-selection": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("spec"), "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required")},
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, driverName, 1)
|
||||||
|
slice.Spec.NodeName = ""
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
},
|
},
|
||||||
"bad-drivername": {
|
"bad-drivername": {
|
||||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "driver"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "driver"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||||
slice: testResourceSlice(goodName, goodName, badName),
|
slice: testResourceSlice(goodName, goodName, badName, 1),
|
||||||
|
},
|
||||||
|
"bad-devices": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.Invalid(field.NewPath("spec", "devices").Index(1).Child("name"), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"),
|
||||||
|
field.Required(field.NewPath("spec", "devices").Index(2).Child("basic"), ""),
|
||||||
|
},
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, goodName, 3)
|
||||||
|
slice.Spec.Devices[1].Name = badName
|
||||||
|
slice.Spec.Devices[2].Basic = nil
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-attribute": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.TypeInvalid(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key(badName), badName, "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"),
|
||||||
|
field.Required(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key(badName), "exactly one value must be specified"),
|
||||||
|
field.Invalid(field.NewPath("spec", "devices").Index(2).Child("basic", "attributes").Key(goodName), resourceapi.DeviceAttribute{StringValue: ptr.To("x"), VersionValue: ptr.To("1.2.3")}, "exactly one value must be specified"),
|
||||||
|
field.Invalid(field.NewPath("spec", "devices").Index(3).Child("basic", "attributes").Key(goodName).Child("version"), strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1), "must be a string compatible with semver.org spec 2.0.0"),
|
||||||
|
field.TooLongMaxLength(field.NewPath("spec", "devices").Index(3).Child("basic", "attributes").Key(goodName).Child("version"), strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1), resourceapi.DeviceAttributeMaxValueLength),
|
||||||
|
field.TooLongMaxLength(field.NewPath("spec", "devices").Index(4).Child("basic", "attributes").Key(goodName).Child("string"), strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1), resourceapi.DeviceAttributeMaxValueLength),
|
||||||
|
},
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, goodName, 5)
|
||||||
|
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
|
||||||
|
resourceapi.QualifiedName(badName): {},
|
||||||
|
}
|
||||||
|
slice.Spec.Devices[2].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
|
||||||
|
resourceapi.QualifiedName(goodName): {StringValue: ptr.To("x"), VersionValue: ptr.To("1.2.3")},
|
||||||
|
}
|
||||||
|
slice.Spec.Devices[3].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
|
||||||
|
resourceapi.QualifiedName(goodName): {VersionValue: ptr.To(strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1))},
|
||||||
|
}
|
||||||
|
slice.Spec.Devices[4].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
|
||||||
|
resourceapi.QualifiedName(goodName): {StringValue: ptr.To(strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1))},
|
||||||
|
}
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"good-attribute-names": {
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, goodName, 2)
|
||||||
|
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
|
||||||
|
resourceapi.QualifiedName(strings.Repeat("x", resourceapi.DeviceMaxIDLength)): {StringValue: ptr.To("y")},
|
||||||
|
resourceapi.QualifiedName(strings.Repeat("x", resourceapi.DeviceMaxDomainLength) + "/" + strings.Repeat("y", resourceapi.DeviceMaxIDLength)): {StringValue: ptr.To("z")},
|
||||||
|
}
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-attribute-c-identifier": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.TooLongMaxLength(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key(strings.Repeat(".", resourceapi.DeviceMaxIDLength+1)), strings.Repeat(".", resourceapi.DeviceMaxIDLength+1), resourceapi.DeviceMaxIDLength),
|
||||||
|
field.TypeInvalid(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key(strings.Repeat(".", resourceapi.DeviceMaxIDLength+1)), strings.Repeat(".", resourceapi.DeviceMaxIDLength+1), "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"),
|
||||||
|
},
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, goodName, 2)
|
||||||
|
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
|
||||||
|
resourceapi.QualifiedName(strings.Repeat(".", resourceapi.DeviceMaxIDLength+1)): {StringValue: ptr.To("y")},
|
||||||
|
}
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-attribute-domain": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.TooLong(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key(strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1)+"/y"), strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1), resourceapi.DeviceMaxDomainLength),
|
||||||
|
field.Invalid(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key(strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1)+"/y"), strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1), "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||||
|
},
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, goodName, 2)
|
||||||
|
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
|
||||||
|
resourceapi.QualifiedName(strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1) + "/y"): {StringValue: ptr.To("z")},
|
||||||
|
}
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-key-too-long": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.TooLong(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...xxxxxxxxxxxx/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"), strings.Repeat("x", resourceapi.DeviceMaxDomainLength+1), resourceapi.DeviceMaxDomainLength),
|
||||||
|
field.TooLongMaxLength(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...xxxxxxxxxxxx/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"), strings.Repeat("y", resourceapi.DeviceMaxIDLength+1), resourceapi.DeviceMaxIDLength),
|
||||||
|
},
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, goodName, 2)
|
||||||
|
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
|
||||||
|
resourceapi.QualifiedName(strings.Repeat("x", resourceapi.DeviceMaxDomainLength+1) + "/" + strings.Repeat("y", resourceapi.DeviceMaxIDLength+1)): {StringValue: ptr.To("z")},
|
||||||
|
}
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-attribute-empty-domain-and-c-identifier": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.Required(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key("/"), "the domain must not be empty"),
|
||||||
|
field.Required(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key("/"), "the name must not be empty"),
|
||||||
|
},
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, goodName, 2)
|
||||||
|
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
|
||||||
|
resourceapi.QualifiedName("/"): {StringValue: ptr.To("z")},
|
||||||
|
}
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"combined-attributes-and-capacity-length": {
|
||||||
|
wantFailures: field.ErrorList{
|
||||||
|
field.Invalid(field.NewPath("spec", "devices").Index(2).Child("basic"), resourceapi.ResourceSliceMaxAttributesAndCapacitiesPerDevice+1, fmt.Sprintf("the total number of attributes and capacities must not exceed %d", resourceapi.ResourceSliceMaxAttributesAndCapacitiesPerDevice)),
|
||||||
|
},
|
||||||
|
slice: func() *resourceapi.ResourceSlice {
|
||||||
|
slice := testResourceSlice(goodName, goodName, goodName, 3)
|
||||||
|
slice.Spec.Devices[0].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{}
|
||||||
|
slice.Spec.Devices[0].Basic.Capacity = map[resourceapi.QualifiedName]resource.Quantity{}
|
||||||
|
for i := 0; i < resourceapi.ResourceSliceMaxAttributesAndCapacitiesPerDevice; i++ {
|
||||||
|
slice.Spec.Devices[0].Basic.Attributes[resourceapi.QualifiedName(fmt.Sprintf("attr_%d", i))] = resourceapi.DeviceAttribute{StringValue: ptr.To("x")}
|
||||||
|
}
|
||||||
|
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{}
|
||||||
|
slice.Spec.Devices[1].Basic.Capacity = map[resourceapi.QualifiedName]resource.Quantity{}
|
||||||
|
quantity := resource.MustParse("1Gi")
|
||||||
|
for i := 0; i < resourceapi.ResourceSliceMaxAttributesAndCapacitiesPerDevice; i++ {
|
||||||
|
slice.Spec.Devices[1].Basic.Capacity[resourceapi.QualifiedName(fmt.Sprintf("cap_%d", i))] = quantity
|
||||||
|
}
|
||||||
|
// Too large together by one.
|
||||||
|
slice.Spec.Devices[2].Basic.Attributes = slice.Spec.Devices[0].Basic.Attributes
|
||||||
|
slice.Spec.Devices[2].Basic.Capacity = map[resourceapi.QualifiedName]resource.Quantity{
|
||||||
|
"cap": quantity,
|
||||||
|
}
|
||||||
|
return slice
|
||||||
|
}(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, scenario := range scenarios {
|
for name, scenario := range scenarios {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
errs := ValidateResourceSlice(scenario.slice)
|
errs := ValidateResourceSlice(scenario.slice)
|
||||||
assert.Equal(t, scenario.wantFailures, errs)
|
assertFailures(t, scenario.wantFailures, errs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateResourceSliceUpdate(t *testing.T) {
|
func TestValidateResourceSliceUpdate(t *testing.T) {
|
||||||
name := "valid"
|
name := "valid"
|
||||||
validResourceSlice := testResourceSlice(name, name, name)
|
validResourceSlice := testResourceSlice(name, name, name, 1)
|
||||||
|
|
||||||
scenarios := map[string]struct {
|
scenarios := map[string]struct {
|
||||||
oldResourceSlice *resource.ResourceSlice
|
oldResourceSlice *resourceapi.ResourceSlice
|
||||||
update func(slice *resource.ResourceSlice) *resource.ResourceSlice
|
update func(slice *resourceapi.ResourceSlice) *resourceapi.ResourceSlice
|
||||||
wantFailures field.ErrorList
|
wantFailures field.ErrorList
|
||||||
}{
|
}{
|
||||||
"valid-no-op-update": {
|
"valid-no-op-update": {
|
||||||
oldResourceSlice: validResourceSlice,
|
oldResourceSlice: validResourceSlice,
|
||||||
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice { return slice },
|
update: func(slice *resourceapi.ResourceSlice) *resourceapi.ResourceSlice { return slice },
|
||||||
},
|
},
|
||||||
"invalid-name-update": {
|
"invalid-name-update": {
|
||||||
oldResourceSlice: validResourceSlice,
|
oldResourceSlice: validResourceSlice,
|
||||||
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
|
update: func(slice *resourceapi.ResourceSlice) *resourceapi.ResourceSlice {
|
||||||
slice.Name += "-update"
|
slice.Name += "-update"
|
||||||
return slice
|
return slice
|
||||||
},
|
},
|
||||||
@@ -235,7 +463,7 @@ func TestValidateResourceSliceUpdate(t *testing.T) {
|
|||||||
"invalid-update-nodename": {
|
"invalid-update-nodename": {
|
||||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "nodeName"), name+"-updated", "field is immutable")},
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "nodeName"), name+"-updated", "field is immutable")},
|
||||||
oldResourceSlice: validResourceSlice,
|
oldResourceSlice: validResourceSlice,
|
||||||
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
|
update: func(slice *resourceapi.ResourceSlice) *resourceapi.ResourceSlice {
|
||||||
slice.Spec.NodeName += "-updated"
|
slice.Spec.NodeName += "-updated"
|
||||||
return slice
|
return slice
|
||||||
},
|
},
|
||||||
@@ -243,7 +471,7 @@ func TestValidateResourceSliceUpdate(t *testing.T) {
|
|||||||
"invalid-update-drivername": {
|
"invalid-update-drivername": {
|
||||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "driver"), name+"-updated", "field is immutable")},
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "driver"), name+"-updated", "field is immutable")},
|
||||||
oldResourceSlice: validResourceSlice,
|
oldResourceSlice: validResourceSlice,
|
||||||
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
|
update: func(slice *resourceapi.ResourceSlice) *resourceapi.ResourceSlice {
|
||||||
slice.Spec.Driver += "-updated"
|
slice.Spec.Driver += "-updated"
|
||||||
return slice
|
return slice
|
||||||
},
|
},
|
||||||
@@ -251,7 +479,7 @@ func TestValidateResourceSliceUpdate(t *testing.T) {
|
|||||||
"invalid-update-pool": {
|
"invalid-update-pool": {
|
||||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "pool", "name"), validResourceSlice.Spec.Pool.Name+"-updated", "field is immutable")},
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "pool", "name"), validResourceSlice.Spec.Pool.Name+"-updated", "field is immutable")},
|
||||||
oldResourceSlice: validResourceSlice,
|
oldResourceSlice: validResourceSlice,
|
||||||
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
|
update: func(slice *resourceapi.ResourceSlice) *resourceapi.ResourceSlice {
|
||||||
slice.Spec.Pool.Name += "-updated"
|
slice.Spec.Pool.Name += "-updated"
|
||||||
return slice
|
return slice
|
||||||
},
|
},
|
||||||
@@ -262,7 +490,7 @@ func TestValidateResourceSliceUpdate(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
scenario.oldResourceSlice.ResourceVersion = "1"
|
scenario.oldResourceSlice.ResourceVersion = "1"
|
||||||
errs := ValidateResourceSliceUpdate(scenario.update(scenario.oldResourceSlice.DeepCopy()), scenario.oldResourceSlice)
|
errs := ValidateResourceSliceUpdate(scenario.update(scenario.oldResourceSlice.DeepCopy()), scenario.oldResourceSlice)
|
||||||
assert.Equal(t, scenario.wantFailures, errs)
|
assertFailures(t, scenario.wantFailures, errs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -244,6 +244,9 @@ type QualifiedName string
|
|||||||
// FullyQualifiedName is a QualifiedName where the domain is set.
|
// FullyQualifiedName is a QualifiedName where the domain is set.
|
||||||
type FullyQualifiedName string
|
type FullyQualifiedName string
|
||||||
|
|
||||||
|
// DeviceMaxDomainLength is the maximum length of the domain prefix in a fully-qualified name.
|
||||||
|
const DeviceMaxDomainLength = 63
|
||||||
|
|
||||||
// DeviceMaxIDLength is the maximum length of the identifier in a device attribute or capacity name (`<domain>/<ID>`).
|
// DeviceMaxIDLength is the maximum length of the identifier in a device attribute or capacity name (`<domain>/<ID>`).
|
||||||
const DeviceMaxIDLength = 32
|
const DeviceMaxIDLength = 32
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user