DRA: new API for 1.31

This is a complete revamp of the original API. Some of the key
differences:
- refocused on structured parameters and allocating devices
- support for constraints across devices
- support for allocating "all" or a fixed amount
  of similar devices in a single request
- no class for ResourceClaims, instead individual
  device requests are associated with a mandatory
  DeviceClass

For the sake of simplicity, optional basic types (ints, strings) where the null
value is the default are represented as values in the API types. This makes Go
code simpler because it doesn't have to check for nil (consumers) and values
can be set directly (producers). The effect is that in protobuf, these fields
always get encoded because `opt` only has an effect for pointers.

The roundtrip test data for v1.29.0 and v1.30.0 changes because of the new
"request" field. This is considered acceptable because the entire `claims`
field in the pod spec is still alpha.

The implementation is complete enough to bring up the apiserver.
Adapting other components follows.
This commit is contained in:
Patrick Ohly
2024-06-18 17:47:29 +02:00
parent bcececadfb
commit 91d7882e86
306 changed files with 16480 additions and 26466 deletions

View File

@@ -2455,6 +2455,13 @@ type ResourceClaim struct {
// the Pod where this field is used. It makes that resource available
// inside a container.
Name string
// Request is the name chosen for a request in the referenced claim.
// If empty, everything from the claim is made available, otherwise
// only the result of this request.
//
// +optional
Request string
}
// Container represents a single container that is expected to be run on the host.

View File

@@ -7439,6 +7439,7 @@ func Convert_core_ReplicationControllerStatus_To_v1_ReplicationControllerStatus(
func autoConvert_v1_ResourceClaim_To_core_ResourceClaim(in *v1.ResourceClaim, out *core.ResourceClaim, s conversion.Scope) error {
out.Name = in.Name
out.Request = in.Request
return nil
}
@@ -7449,6 +7450,7 @@ func Convert_v1_ResourceClaim_To_core_ResourceClaim(in *v1.ResourceClaim, out *c
func autoConvert_core_ResourceClaim_To_v1_ResourceClaim(in *core.ResourceClaim, out *v1.ResourceClaim, s conversion.Scope) error {
out.Name = in.Name
out.Request = in.Request
return nil
}

View File

@@ -6757,9 +6757,35 @@ func validateResourceClaimNames(claims []core.ResourceClaim, podClaimNames sets.
allErrs = append(allErrs, field.Required(fldPath.Index(i), ""))
} else {
if names.Has(name) {
// All requests of that claim already referenced.
allErrs = append(allErrs, field.Duplicate(fldPath.Index(i), name))
} else {
names.Insert(name)
key := name
if claim.Request != "" {
allErrs = append(allErrs, ValidateDNS1123Label(claim.Request, fldPath.Index(i).Child("request"))...)
key += "/" + claim.Request
}
if names.Has(key) {
// The exact same entry was already referenced.
allErrs = append(allErrs, field.Duplicate(fldPath.Index(i), key))
} else if claim.Request == "" {
// When referencing a claim, there's an
// overlap when previously some request
// in the claim was referenced. This
// cannot be checked with a map lookup,
// we need to iterate.
for key := range names {
index := strings.Index(key, "/")
if index < 0 {
continue
}
if key[0:index] == name {
allErrs = append(allErrs, field.Duplicate(fldPath.Index(i), name))
}
}
}
names.Insert(key)
}
if !podClaimNames.Has(name) {
// field.NotFound doesn't accept an

View File

@@ -23816,6 +23816,8 @@ func TestValidateDynamicResourceAllocation(t *testing.T) {
shortPodName := &metav1.ObjectMeta{
Name: "some-pod",
}
requestName := "req-0"
anotherRequestName := "req-1"
goodClaimTemplate := podtest.MakePod("",
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim-template"}}}))),
podtest.SetRestartPolicy(core.RestartPolicyAlways),
@@ -23848,6 +23850,26 @@ func TestValidateDynamicResourceAllocation(t *testing.T) {
ResourceClaimName: &externalClaimName,
}),
),
"multiple claims with requests": podtest.MakePod("",
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim", Request: requestName}, {Name: "another-claim", Request: requestName}}}))),
podtest.SetResourceClaims(
core.PodResourceClaim{
Name: "my-claim",
ResourceClaimName: &externalClaimName,
},
core.PodResourceClaim{
Name: "another-claim",
ResourceClaimName: &externalClaimName,
}),
),
"single claim with requests": podtest.MakePod("",
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim", Request: requestName}, {Name: "my-claim", Request: anotherRequestName}}}))),
podtest.SetResourceClaims(
core.PodResourceClaim{
Name: "my-claim",
ResourceClaimName: &externalClaimName,
}),
),
"init container": podtest.MakePod("",
podtest.SetInitContainers(podtest.MakeContainer("ctr-init", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}}}))),
podtest.SetResourceClaims(core.PodResourceClaim{
@@ -23928,6 +23950,34 @@ func TestValidateDynamicResourceAllocation(t *testing.T) {
ResourceClaimName: &externalClaimName,
}),
),
"pod claim name duplicates without and with request": podtest.MakePod("",
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}, {Name: "my-claim", Request: "req-0"}}}))),
podtest.SetResourceClaims(core.PodResourceClaim{
Name: "my-claim",
ResourceClaimName: &externalClaimName,
}),
),
"pod claim name duplicates with and without request": podtest.MakePod("",
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim", Request: "req-0"}, {Name: "my-claim"}}}))),
podtest.SetResourceClaims(core.PodResourceClaim{
Name: "my-claim",
ResourceClaimName: &externalClaimName,
}),
),
"pod claim name duplicates with requests": podtest.MakePod("",
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim", Request: "req-0"}, {Name: "my-claim", Request: "req-0"}}}))),
podtest.SetResourceClaims(core.PodResourceClaim{
Name: "my-claim",
ResourceClaimName: &externalClaimName,
}),
),
"bad request name": podtest.MakePod("",
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim", Request: "*$@%^"}}}))),
podtest.SetResourceClaims(core.PodResourceClaim{
Name: "my-claim",
ResourceClaimName: &externalClaimName,
}),
),
"no claims defined": podtest.MakePod("",
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}}}))),
podtest.SetRestartPolicy(core.RestartPolicyAlways),

View File

@@ -17,10 +17,44 @@ limitations under the License.
package fuzzer
import (
fuzz "github.com/google/gofuzz"
"k8s.io/apimachinery/pkg/runtime"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/kubernetes/pkg/apis/resource"
)
// Funcs contains the fuzzer functions for the resource group.
//
// Leaving fields empty which then get replaced by the default
// leads to errors during roundtrip tests.
var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
return nil
return []interface{}{
func(r *resource.DeviceRequest, c fuzz.Continue) {
c.FuzzNoCustom(r) // fuzz self without calling this function again
if r.AllocationMode == "" {
r.AllocationMode = []resource.DeviceAllocationMode{
resource.DeviceAllocationModeAll,
resource.DeviceAllocationModeExactCount,
}[c.Int31n(2)]
}
},
func(r *resource.DeviceAllocationConfiguration, c fuzz.Continue) {
c.FuzzNoCustom(r)
if r.Source == "" {
r.Source = []resource.AllocationConfigSource{
resource.AllocationConfigSourceClass,
resource.AllocationConfigSourceClaim,
}[c.Int31n(2)]
}
},
func(r *resource.OpaqueDeviceConfiguration, c fuzz.Continue) {
c.FuzzNoCustom(r)
// Match the fuzzer default content for runtime.Object.
//
// This is necessary because randomly generated content
// might be valid JSON which changes during re-encoding.
r.Parameters = runtime.RawExtension{Raw: []byte(`{"apiVersion":"unknown.group/unknown","kind":"Something","someKey":"someValue"}`)}
},
}
}

View File

@@ -1,112 +0,0 @@
/*
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 resource
import "k8s.io/apimachinery/pkg/api/resource"
// NamedResourcesResources is used in ResourceModel.
type NamedResourcesResources struct {
// The list of all individual resources instances currently available.
Instances []NamedResourcesInstance
}
// NamedResourcesInstance represents one individual hardware instance that can be selected based
// on its attributes.
type NamedResourcesInstance struct {
// Name is unique identifier among all resource instances managed by
// the driver on the node. It must be a DNS subdomain.
Name string
// Attributes defines the attributes of this resource instance.
// The name of each attribute must be unique.
Attributes []NamedResourcesAttribute
}
// NamedResourcesAttribute is a combination of an attribute name and its value.
type NamedResourcesAttribute struct {
// Name is unique identifier among all resource instances managed by
// the driver on the node. It must be a DNS subdomain.
Name string
NamedResourcesAttributeValue
}
// NamedResourcesAttributeValue must have one and only one field set.
type NamedResourcesAttributeValue struct {
// QuantityValue is a quantity.
QuantityValue *resource.Quantity
// BoolValue is a true/false value.
BoolValue *bool
// IntValue is a 64-bit integer.
IntValue *int64
// IntSliceValue is an array of 64-bit integers.
IntSliceValue *NamedResourcesIntSlice
// StringValue is a string.
StringValue *string
// StringSliceValue is an array of strings.
StringSliceValue *NamedResourcesStringSlice
// VersionValue is a semantic version according to semver.org spec 2.0.0.
VersionValue *string
}
// NamedResourcesIntSlice contains a slice of 64-bit integers.
type NamedResourcesIntSlice struct {
// Ints is the slice of 64-bit integers.
Ints []int64
}
// NamedResourcesStringSlice contains a slice of strings.
type NamedResourcesStringSlice struct {
// Strings is the slice of strings.
Strings []string
}
// NamedResourcesRequest is used in ResourceRequestModel.
type NamedResourcesRequest struct {
// Selector is a CEL expression which must evaluate to true if a
// resource instance is suitable. The language is as defined in
// https://kubernetes.io/docs/reference/using-api/cel/
//
// In addition, for each type NamedResourcesin AttributeValue there is a map that
// resolves to the corresponding value of the instance under evaluation.
// For example:
//
// attributes.quantity["a"].isGreaterThan(quantity("0")) &&
// attributes.stringslice["b"].isSorted()
Selector string
}
// NamedResourcesFilter is used in ResourceFilterModel.
type NamedResourcesFilter struct {
// Selector is a CEL expression which must evaluate to true if a
// resource instance is suitable. The language is as defined in
// https://kubernetes.io/docs/reference/using-api/cel/
//
// In addition, for each type in NamedResourcesAttributeValue there is a map that
// resolves to the corresponding value of the instance under evaluation.
// For example:
//
// attributes.quantity["a"].isGreaterThan(quantity("0")) &&
// attributes.stringslice["b"].isSorted()
Selector string
}
// NamedResourcesAllocationResult is used in AllocationResultModel.
type NamedResourcesAllocationResult struct {
// Name is the name of the selected resource instance.
Name string
}

View File

@@ -52,8 +52,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
return err
}
scheme.AddKnownTypes(SchemeGroupVersion,
&ResourceClass{},
&ResourceClassList{},
&DeviceClass{},
&DeviceClassList{},
&ResourceClaim{},
&ResourceClaimList{},
&ResourceClaimTemplate{},
@@ -62,10 +62,6 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&PodSchedulingContextList{},
&ResourceSlice{},
&ResourceSliceList{},
&ResourceClaimParameters{},
&ResourceClaimParametersList{},
&ResourceClassParameters{},
&ResourceClassParametersList{},
)
return nil

View File

@@ -1,178 +0,0 @@
/*
Copyright 2022 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 (
"fmt"
"regexp"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
namedresourcescel "k8s.io/dynamic-resource-allocation/structured/namedresources/cel"
corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/apis/resource"
)
var (
validateInstanceName = corevalidation.ValidateDNS1123Subdomain
validateAttributeName = corevalidation.ValidateDNS1123Subdomain
)
type Options struct {
// StoredExpressions must be true if and only if validating CEL
// expressions that were already stored persistently. This makes
// validation more permissive by enabling CEL definitions that are not
// valid yet for new expressions.
StoredExpressions bool
}
func ValidateResources(resources *resource.NamedResourcesResources, fldPath *field.Path) field.ErrorList {
allErrs := validateInstances(resources.Instances, fldPath.Child("instances"))
return allErrs
}
func validateInstances(instances []resource.NamedResourcesInstance, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
instanceNames := sets.New[string]()
for i, instance := range instances {
idxPath := fldPath.Index(i)
instanceName := instance.Name
allErrs = append(allErrs, validateInstanceName(instanceName, idxPath.Child("name"))...)
if instanceNames.Has(instanceName) {
allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), instanceName))
} else {
instanceNames.Insert(instanceName)
}
allErrs = append(allErrs, validateAttributes(instance.Attributes, idxPath.Child("attributes"))...)
}
return allErrs
}
var (
numericIdentifier = `(0|[1-9]\d*)`
preReleaseIdentifier = `(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)`
buildIdentifier = `[0-9a-zA-Z-]+`
semverRe = regexp.MustCompile(`^` +
// dot-separated version segments (e.g. 1.2.3)
numericIdentifier + `\.` + numericIdentifier + `\.` + numericIdentifier +
// optional dot-separated prerelease segments (e.g. -alpha.PRERELEASE.1)
`(-` + preReleaseIdentifier + `(\.` + preReleaseIdentifier + `)*)?` +
// optional dot-separated build identifier segments (e.g. +build.id.20240305)
`(\+` + buildIdentifier + `(\.` + buildIdentifier + `)*)?` +
`$`)
)
func validateAttributes(attributes []resource.NamedResourcesAttribute, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
attributeNames := sets.New[string]()
for i, attribute := range attributes {
idxPath := fldPath.Index(i)
attributeName := attribute.Name
allErrs = append(allErrs, validateAttributeName(attributeName, idxPath.Child("name"))...)
if attributeNames.Has(attributeName) {
allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), attributeName))
} else {
attributeNames.Insert(attributeName)
}
entries := sets.New[string]()
if attribute.QuantityValue != nil {
entries.Insert("quantity")
}
if attribute.BoolValue != nil {
entries.Insert("bool")
}
if attribute.IntValue != nil {
entries.Insert("int")
}
if attribute.IntSliceValue != nil {
entries.Insert("intSlice")
}
if attribute.StringValue != nil {
entries.Insert("string")
}
if attribute.StringSliceValue != nil {
entries.Insert("stringSlice")
}
if attribute.VersionValue != nil {
entries.Insert("version")
if !semverRe.MatchString(*attribute.VersionValue) {
allErrs = append(allErrs, field.Invalid(idxPath.Child("version"), *attribute.VersionValue, "must be a string compatible with semver.org spec 2.0.0"))
}
}
switch len(entries) {
case 0:
allErrs = append(allErrs, field.Required(idxPath, "exactly one value must be set"))
case 1:
// Okay.
default:
allErrs = append(allErrs, field.Invalid(idxPath, sets.List(entries), "exactly one field must be set, not several"))
}
}
return allErrs
}
func ValidateRequest(opts Options, request *resource.NamedResourcesRequest, fldPath *field.Path) field.ErrorList {
return validateSelector(opts, request.Selector, fldPath.Child("selector"))
}
func ValidateFilter(opts Options, filter *resource.NamedResourcesFilter, fldPath *field.Path) field.ErrorList {
return validateSelector(opts, filter.Selector, fldPath.Child("selector"))
}
func validateSelector(opts Options, selector string, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if selector == "" {
allErrs = append(allErrs, field.Required(fldPath, ""))
} else {
envType := environment.NewExpressions
if opts.StoredExpressions {
envType = environment.StoredExpressions
}
result := namedresourcescel.GetCompiler().CompileCELExpression(selector, envType)
if result.Error != nil {
allErrs = append(allErrs, convertCELErrorToValidationError(fldPath, selector, result.Error))
}
}
return allErrs
}
func convertCELErrorToValidationError(fldPath *field.Path, expression string, err *cel.Error) *field.Error {
switch err.Type {
case cel.ErrorTypeRequired:
return field.Required(fldPath, err.Detail)
case cel.ErrorTypeInvalid:
return field.Invalid(fldPath, expression, err.Detail)
case cel.ErrorTypeInternal:
return field.InternalError(fldPath, err)
}
return field.InternalError(fldPath, fmt.Errorf("unsupported error type: %w", err))
}
func ValidateAllocationResult(result *resource.NamedResourcesAllocationResult, fldPath *field.Path) field.ErrorList {
return validateInstanceName(result.Name, fldPath.Child("name"))
}

View File

@@ -1,188 +0,0 @@
/*
Copyright 2022 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/api/resource"
"k8s.io/apimachinery/pkg/util/validation/field"
resourceapi "k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/utils/ptr"
)
func testResources(instances []resourceapi.NamedResourcesInstance) *resourceapi.NamedResourcesResources {
resources := &resourceapi.NamedResourcesResources{
Instances: instances,
}
return resources
}
func TestValidateResources(t *testing.T) {
goodName := "foo"
badName := "!@#$%^"
quantity := resource.MustParse("1")
scenarios := map[string]struct {
resources *resourceapi.NamedResourcesResources
wantFailures field.ErrorList
}{
"empty": {
resources: testResources(nil),
},
"good": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName}}),
},
"bad-name": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("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])?)*')")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: badName}}),
},
"duplicate-name": {
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("instances").Index(1).Child("name"), goodName)},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName}, {Name: goodName}}),
},
"quantity": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{QuantityValue: &quantity}}}}}),
},
"bool": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{BoolValue: ptr.To(true)}}}}}),
},
"int": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{IntValue: ptr.To(int64(1))}}}}}),
},
"int-slice": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{IntSliceValue: &resourceapi.NamedResourcesIntSlice{Ints: []int64{1, 2, 3}}}}}}}),
},
"string": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{StringValue: ptr.To("hello")}}}}}),
},
"string-slice": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{StringSliceValue: &resourceapi.NamedResourcesStringSlice{Strings: []string{"hello"}}}}}}}),
},
"version-okay": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0")}}}}}),
},
"version-beta": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta")}}}}}),
},
"version-beta-1": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta.1")}}}}}),
},
"version-build": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0+build")}}}}}),
},
"version-build-1": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0+build.1")}}}}}),
},
"version-beta-1-build-1": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta.1+build.1")}}}}}),
},
"version-bad": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.0", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0")}}}}}),
},
"version-bad-leading-zeros": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "01.0.0", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("01.0.0")}}}}}),
},
"version-bad-leading-zeros-middle": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.00.0", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.00.0")}}}}}),
},
"version-bad-leading-zeros-end": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.0.00", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.00")}}}}}),
},
"version-bad-spaces": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), " 1.0.0 ", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To(" 1.0.0 ")}}}}}),
},
"empty-attribute": {
wantFailures: field.ErrorList{field.Required(field.NewPath("instances").Index(0).Child("attributes").Index(0), "exactly one value must be set")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName}}}}),
},
"duplicate-value": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0), []string{"bool", "int"}, "exactly one field must be set, not several")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{BoolValue: ptr.To(true), IntValue: ptr.To(int64(1))}}}}}),
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
errs := ValidateResources(scenario.resources, nil)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}
func TestValidateSelector(t *testing.T) {
scenarios := map[string]struct {
selector string
wantFailures field.ErrorList
}{
"okay": {
selector: "true",
},
"empty": {
selector: "",
wantFailures: field.ErrorList{field.Required(nil, "")},
},
"undefined": {
selector: "nosuchvar",
wantFailures: field.ErrorList{field.Invalid(nil, "nosuchvar", "compilation failed: ERROR: <input>:1:1: undeclared reference to 'nosuchvar' (in container '')\n | nosuchvar\n | ^")},
},
"wrong-type": {
selector: "1",
wantFailures: field.ErrorList{field.Invalid(nil, "1", "must evaluate to bool")},
},
"quantity": {
selector: `attributes.quantity["name"].isGreaterThan(quantity("0"))`,
},
"bool": {
selector: `attributes.bool["name"]`,
},
"int": {
selector: `attributes.int["name"] > 0`,
},
"intslice": {
selector: `attributes.intslice["name"].isSorted()`,
},
"string": {
selector: `attributes.string["name"] == "fish"`,
},
"stringslice": {
selector: `attributes.stringslice["name"].isSorted()`,
},
"version": {
selector: `attributes.version["name"].isGreaterThan(semver("1.0.0"))`,
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
// At the moment, there's no difference between stored and new expressions.
// This uses the stricter validation.
opts := Options{
StoredExpressions: false,
}
errs := validateSelector(opts, scenario.selector, nil)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,7 @@ package v1alpha3
import (
"fmt"
resourceapi "k8s.io/api/resource/v1alpha3"
"k8s.io/apimachinery/pkg/runtime"
)
@@ -26,7 +27,7 @@ func addConversionFuncs(scheme *runtime.Scheme) error {
if err := scheme.AddFieldLabelConversionFunc(SchemeGroupVersion.WithKind("ResourceSlice"),
func(label, value string) (string, string, error) {
switch label {
case "metadata.name", "nodeName", "driverName":
case "metadata.name", resourceapi.ResourceSliceSelectorNodeName, resourceapi.ResourceSliceSelectorDriver:
return label, value, nil
default:
return "", "", fmt.Errorf("field label not supported for %s: %s", SchemeGroupVersion.WithKind("ResourceSlice"), label)

View File

@@ -17,9 +17,20 @@ limitations under the License.
package v1alpha3
import (
resourceapi "k8s.io/api/resource/v1alpha3"
"k8s.io/apimachinery/pkg/runtime"
)
func addDefaultingFuncs(scheme *runtime.Scheme) error {
return RegisterDefaults(scheme)
}
func SetDefaults_DeviceRequest(obj *resourceapi.DeviceRequest) {
if obj.AllocationMode == "" {
obj.AllocationMode = resourceapi.DeviceAllocationModeExactCount
}
if obj.AllocationMode == resourceapi.DeviceAllocationModeExactCount && obj.Count == 0 {
obj.Count = 1
}
}

View File

@@ -0,0 +1,86 @@
/*
Copyright 2022 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 v1alpha3_test
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
v1alpha3 "k8s.io/api/resource/v1alpha3"
"k8s.io/apimachinery/pkg/runtime"
// ensure types are installed
"k8s.io/kubernetes/pkg/api/legacyscheme"
_ "k8s.io/kubernetes/pkg/apis/resource/install"
)
func TestSetDefaultAllocationMode(t *testing.T) {
claim := &v1alpha3.ResourceClaim{
Spec: v1alpha3.ResourceClaimSpec{
Devices: v1alpha3.DeviceClaim{
Requests: []v1alpha3.DeviceRequest{{}},
},
},
}
// fields should be defaulted
defaultMode := v1alpha3.DeviceAllocationModeExactCount
defaultCount := int64(1)
output := roundTrip(t, runtime.Object(claim)).(*v1alpha3.ResourceClaim)
assert.Equal(t, defaultMode, output.Spec.Devices.Requests[0].AllocationMode)
assert.Equal(t, defaultCount, output.Spec.Devices.Requests[0].Count)
// field should not change
nonDefaultMode := v1alpha3.DeviceAllocationModeExactCount
nonDefaultCount := int64(10)
claim = &v1alpha3.ResourceClaim{
Spec: v1alpha3.ResourceClaimSpec{
Devices: v1alpha3.DeviceClaim{
Requests: []v1alpha3.DeviceRequest{{
AllocationMode: nonDefaultMode,
Count: nonDefaultCount,
}},
},
},
}
output = roundTrip(t, runtime.Object(claim)).(*v1alpha3.ResourceClaim)
assert.Equal(t, nonDefaultMode, output.Spec.Devices.Requests[0].AllocationMode)
assert.Equal(t, nonDefaultCount, output.Spec.Devices.Requests[0].Count)
}
func roundTrip(t *testing.T, obj runtime.Object) runtime.Object {
codec := legacyscheme.Codecs.LegacyCodec(v1alpha3.SchemeGroupVersion)
data, err := runtime.Encode(codec, obj)
if err != nil {
t.Errorf("%v\n %#v", err, obj)
return nil
}
obj2, err := runtime.Decode(codec, data)
if err != nil {
t.Errorf("%v\nData: %s\nSource: %#v", err, string(data), obj)
return nil
}
obj3 := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(runtime.Object)
err = legacyscheme.Scheme.Convert(obj2, obj3, nil)
if err != nil {
t.Errorf("%v\nSource: %#v", err, obj2)
return nil
}
return obj3
}

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@ limitations under the License.
package v1alpha3
import (
v1alpha3 "k8s.io/api/resource/v1alpha3"
runtime "k8s.io/apimachinery/pkg/runtime"
)
@@ -29,5 +30,39 @@ import (
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
scheme.AddTypeDefaultingFunc(&v1alpha3.ResourceClaim{}, func(obj interface{}) { SetObjectDefaults_ResourceClaim(obj.(*v1alpha3.ResourceClaim)) })
scheme.AddTypeDefaultingFunc(&v1alpha3.ResourceClaimList{}, func(obj interface{}) { SetObjectDefaults_ResourceClaimList(obj.(*v1alpha3.ResourceClaimList)) })
scheme.AddTypeDefaultingFunc(&v1alpha3.ResourceClaimTemplate{}, func(obj interface{}) { SetObjectDefaults_ResourceClaimTemplate(obj.(*v1alpha3.ResourceClaimTemplate)) })
scheme.AddTypeDefaultingFunc(&v1alpha3.ResourceClaimTemplateList{}, func(obj interface{}) {
SetObjectDefaults_ResourceClaimTemplateList(obj.(*v1alpha3.ResourceClaimTemplateList))
})
return nil
}
func SetObjectDefaults_ResourceClaim(in *v1alpha3.ResourceClaim) {
for i := range in.Spec.Devices.Requests {
a := &in.Spec.Devices.Requests[i]
SetDefaults_DeviceRequest(a)
}
}
func SetObjectDefaults_ResourceClaimList(in *v1alpha3.ResourceClaimList) {
for i := range in.Items {
a := &in.Items[i]
SetObjectDefaults_ResourceClaim(a)
}
}
func SetObjectDefaults_ResourceClaimTemplate(in *v1alpha3.ResourceClaimTemplate) {
for i := range in.Spec.Spec.Devices.Requests {
a := &in.Spec.Spec.Devices.Requests[i]
SetDefaults_DeviceRequest(a)
}
}
func SetObjectDefaults_ResourceClaimTemplateList(in *v1alpha3.ResourceClaimTemplateList) {
for i := range in.Items {
a := &in.Items[i]
SetObjectDefaults_ResourceClaimTemplate(a)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,247 @@
/*
Copyright 2022 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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/utils/ptr"
)
func testClass(name string) *resource.DeviceClass {
return &resource.DeviceClass{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}
}
func TestValidateClass(t *testing.T) {
goodName := "foo"
now := metav1.Now()
badName := "!@#$%^"
badValue := "spaces not allowed"
scenarios := map[string]struct {
class *resource.DeviceClass
wantFailures field.ErrorList
}{
"good-class": {
class: testClass(goodName),
},
"missing-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
class: testClass(""),
},
"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])?)*')")},
class: testClass(badName),
},
"generate-name": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.GenerateName = "pvc-"
return class
}(),
},
"uid": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
return class
}(),
},
"resource-version": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.ResourceVersion = "1"
return class
}(),
},
"generation": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Generation = 100
return class
}(),
},
"creation-timestamp": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.CreationTimestamp = now
return class
}(),
},
"deletion-grace-period-seconds": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.DeletionGracePeriodSeconds = ptr.To(int64(10))
return class
}(),
},
"owner-references": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "pod",
Name: "foo",
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
},
}
return class
}(),
},
"finalizers": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Finalizers = []string{
"example.com/foo",
}
return class
}(),
},
"managed-fields": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.ManagedFields = []metav1.ManagedFieldsEntry{
{
FieldsType: "FieldsV1",
Operation: "Apply",
APIVersion: "apps/v1",
Manager: "foo",
},
}
return class
}(),
},
"good-labels": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Labels = map[string]string{
"apps.kubernetes.io/name": "test",
}
return class
}(),
},
"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])?')")},
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Labels = map[string]string{
"hello-world": badValue,
}
return class
}(),
},
"good-annotations": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Annotations = map[string]string{
"foo": "bar",
}
return class
}(),
},
"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]')")},
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Annotations = map[string]string{
badName: "hello world",
}
return class
}(),
},
"invalid-node-selector": {
wantFailures: field.ErrorList{field.Required(field.NewPath("suitableNodes", "nodeSelectorTerms"), "must have at least one node selector term")},
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Spec.SuitableNodes = &core.NodeSelector{
// Must not be empty.
}
return class
}(),
},
"valid-node-selector": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Spec.SuitableNodes = &core.NodeSelector{
NodeSelectorTerms: []core.NodeSelectorTerm{{
MatchExpressions: []core.NodeSelectorRequirement{{
Key: "foo",
Operator: core.NodeSelectorOpDoesNotExist,
}},
}},
}
return class
}(),
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
errs := ValidateDeviceClass(scenario.class)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}
func TestValidateClassUpdate(t *testing.T) {
validClass := testClass(goodName)
scenarios := map[string]struct {
oldClass *resource.DeviceClass
update func(class *resource.DeviceClass) *resource.DeviceClass
wantFailures field.ErrorList
}{
"valid-no-op-update": {
oldClass: validClass,
update: func(class *resource.DeviceClass) *resource.DeviceClass { return class },
},
"update-node-selector": {
oldClass: validClass,
update: func(class *resource.DeviceClass) *resource.DeviceClass {
class = class.DeepCopy()
class.Spec.SuitableNodes = &core.NodeSelector{
NodeSelectorTerms: []core.NodeSelectorTerm{{
MatchExpressions: []core.NodeSelectorRequirement{{
Key: "foo",
Operator: core.NodeSelectorOpDoesNotExist,
}},
}},
}
return class
},
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
scenario.oldClass.ResourceVersion = "1"
errs := ValidateDeviceClassUpdate(scenario.update(scenario.oldClass.DeepCopy()), scenario.oldClass)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}

View File

@@ -18,17 +18,18 @@ package validation
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/utils/pointer"
"k8s.io/utils/ptr"
)
func testClaim(name, namespace string, spec resource.ResourceClaimSpec) *resource.ResourceClaim {
@@ -37,86 +38,98 @@ func testClaim(name, namespace string, spec resource.ResourceClaimSpec) *resourc
Name: name,
Namespace: namespace,
},
Spec: spec,
Spec: *spec.DeepCopy(),
}
}
func TestValidateClaim(t *testing.T) {
goodName := "foo"
badName := "!@#$%^"
goodNS := "ns"
goodClaimSpec := resource.ResourceClaimSpec{
ResourceClassName: goodName,
const (
goodName = "foo"
badName = "!@#$%^"
goodNS = "ns"
)
var (
validClaimSpec = resource.ResourceClaimSpec{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{{
Name: goodName,
DeviceClassName: goodName,
AllocationMode: resource.DeviceAllocationModeExactCount,
Count: 1,
}},
},
}
validClaim = testClaim(goodName, goodNS, validClaimSpec)
)
func TestValidateClaim(t *testing.T) {
now := metav1.Now()
badValue := "spaces not allowed"
badAPIGroup := "example.com/v1"
goodAPIGroup := "example.com"
scenarios := map[string]struct {
claim *resource.ResourceClaim
wantFailures field.ErrorList
}{
"good-claim": {
claim: testClaim(goodName, goodNS, goodClaimSpec),
claim: testClaim(goodName, goodNS, validClaimSpec),
},
"missing-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
claim: testClaim("", goodNS, goodClaimSpec),
claim: testClaim("", goodNS, validClaimSpec),
},
"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])?)*')")},
claim: testClaim(badName, goodNS, goodClaimSpec),
claim: testClaim(badName, goodNS, validClaimSpec),
},
"missing-namespace": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
claim: testClaim(goodName, "", goodClaimSpec),
claim: testClaim(goodName, "", validClaimSpec),
},
"generate-name": {
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.GenerateName = "pvc-"
return claim
}(),
},
"uid": {
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
return claim
}(),
},
"resource-version": {
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.ResourceVersion = "1"
return claim
}(),
},
"generation": {
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Generation = 100
return claim
}(),
},
"creation-timestamp": {
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.CreationTimestamp = now
return claim
}(),
},
"deletion-grace-period-seconds": {
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.DeletionGracePeriodSeconds = pointer.Int64(10)
return claim
}(),
},
"owner-references": {
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "v1",
@@ -130,7 +143,7 @@ func TestValidateClaim(t *testing.T) {
},
"finalizers": {
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Finalizers = []string{
"example.com/foo",
}
@@ -139,7 +152,7 @@ func TestValidateClaim(t *testing.T) {
},
"managed-fields": {
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.ManagedFields = []metav1.ManagedFieldsEntry{
{
FieldsType: "FieldsV1",
@@ -153,7 +166,7 @@ func TestValidateClaim(t *testing.T) {
},
"good-labels": {
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Labels = map[string]string{
"apps.kubernetes.io/name": "test",
}
@@ -163,7 +176,7 @@ func TestValidateClaim(t *testing.T) {
"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])?')")},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Labels = map[string]string{
"hello-world": badValue,
}
@@ -172,7 +185,7 @@ func TestValidateClaim(t *testing.T) {
},
"good-annotations": {
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Annotations = map[string]string{
"foo": "bar",
}
@@ -182,7 +195,7 @@ func TestValidateClaim(t *testing.T) {
"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]')")},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Annotations = map[string]string{
badName: "hello world",
}
@@ -190,62 +203,115 @@ func TestValidateClaim(t *testing.T) {
}(),
},
"bad-classname": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "resourceClassName"), 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", "devices", "requests").Index(0).Child("deviceClassName"), 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])?)*')")},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim.Spec.ResourceClassName = badName
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Spec.Devices.Requests[0].DeviceClassName = badName
return claim
}(),
},
"good-parameters": {
"invalid-request-name": {
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, "must be the name of a request in the claim"),
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"),
},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
Kind: "foo",
Name: "bar",
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("dra.example.com/numa")),
}}
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"}`),
},
},
},
}}
return claim
}(),
},
"invalid-config-json": {
wantFailures: field.ErrorList{
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(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"), ""),
},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Spec.Devices.Config = []resource.DeviceClaimConfiguration{
{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: "dra.example.com",
Parameters: runtime.RawExtension{
Raw: []byte(``),
},
},
},
},
{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: "dra.example.com",
Parameters: runtime.RawExtension{
Raw: []byte(`{`),
},
},
},
},
{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: "dra.example.com",
Parameters: runtime.RawExtension{
Raw: []byte(`"hello-world"`),
},
},
},
},
{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: "dra.example.com",
Parameters: runtime.RawExtension{
Raw: []byte(`null`),
},
},
},
},
}
return claim
}(),
},
"good-parameters-apigroup": {
"CEL-compile-errors": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "devices", "requests").Index(1).Child("selectors").Index(1).Child("cel", "expression"), `device.attributes[true].someBoolean`, "compilation failed: ERROR: <input>:1:18: found no matching overload for '_[_]' applied to '(map(string, map(string, any)), bool)'\n | device.attributes[true].someBoolean\n | .................^"),
},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
APIGroup: goodAPIGroup,
Kind: "foo",
Name: "bar",
}
return claim
}(),
},
"bad-parameters-apigroup": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "parametersRef", "apiGroup"), badAPIGroup, "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])?)*')")},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
APIGroup: badAPIGroup,
Kind: "foo",
Name: "bar",
}
return claim
}(),
},
"missing-parameters-kind": {
wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "parametersRef", "kind"), "")},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
Name: "bar",
}
return claim
}(),
},
"missing-parameters-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "parametersRef", "name"), "")},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, goodClaimSpec)
claim.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
Kind: "foo",
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, claim.Spec.Devices.Requests[0])
claim.Spec.Devices.Requests[1].Name += "-2"
claim.Spec.Devices.Requests[1].Selectors = []resource.DeviceSelector{
{
// Good selector.
CEL: &resource.CELDeviceSelector{
Expression: `device.driver == "dra.example.com"`,
},
},
{
// Bad selector.
CEL: &resource.CELDeviceSelector{
Expression: `device.attributes[true].someBoolean`,
},
},
}
return claim
}(),
@@ -254,23 +320,13 @@ func TestValidateClaim(t *testing.T) {
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
errs := ValidateClaim(scenario.claim)
errs := ValidateResourceClaim(scenario.claim)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}
func TestValidateClaimUpdate(t *testing.T) {
name := "valid"
parameters := &resource.ResourceClaimParametersReference{
Kind: "foo",
Name: "bar",
}
validClaim := testClaim("foo", "ns", resource.ResourceClaimSpec{
ResourceClassName: name,
ParametersRef: parameters,
})
scenarios := map[string]struct {
oldClaim *resource.ResourceClaim
update func(claim *resource.ResourceClaim) *resource.ResourceClaim
@@ -283,24 +339,12 @@ func TestValidateClaimUpdate(t *testing.T) {
"invalid-update-class": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimSpec {
spec := validClaim.Spec.DeepCopy()
spec.ResourceClassName += "2"
spec.Devices.Requests[0].DeviceClassName += "2"
return *spec
}(), "field is immutable")},
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Spec.ResourceClassName += "2"
return claim
},
},
"invalid-update-remove-parameters": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimSpec {
spec := validClaim.Spec.DeepCopy()
spec.ParametersRef = nil
return *spec
}(), "field is immutable")},
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Spec.ParametersRef = nil
claim.Spec.Devices.Requests[0].DeviceClassName += "2"
return claim
},
},
@@ -309,33 +353,24 @@ func TestValidateClaimUpdate(t *testing.T) {
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
scenario.oldClaim.ResourceVersion = "1"
errs := ValidateClaimUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim)
errs := ValidateResourceClaimUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}
func TestValidateClaimStatusUpdate(t *testing.T) {
invalidName := "!@#$%^"
validClaim := testClaim("foo", "ns", resource.ResourceClaimSpec{
ResourceClassName: "valid",
})
validAllocatedClaim := validClaim.DeepCopy()
validAllocatedClaim.Status = resource.ResourceClaimStatus{
DriverName: "valid",
Allocation: &resource.AllocationResult{
ResourceHandles: func() []resource.ResourceHandle {
var handles []resource.ResourceHandle
for i := 0; i < resource.AllocationResultResourceHandlesMaxSize; i++ {
handle := resource.ResourceHandle{
DriverName: "valid",
Data: strings.Repeat(" ", resource.ResourceHandleDataMaxSize),
}
handles = append(handles, handle)
}
return handles
}(),
Devices: resource.DeviceAllocationResult{
Results: []resource.DeviceRequestAllocationResult{{
Request: goodName,
Driver: goodName,
Pool: goodName,
Device: goodName,
}},
},
},
}
@@ -348,176 +383,55 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { return claim },
},
"add-driver": {
"valid-add-allocation-empty": {
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Status.DriverName = "valid"
return claim
},
},
"invalid-add-allocation": {
wantFailures: field.ErrorList{field.Required(field.NewPath("status", "driverName"), "must be specified when `allocation` is set")},
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
// DriverName must also get set here!
claim.Status.Allocation = &resource.AllocationResult{}
return claim
},
},
"valid-add-allocation": {
"valid-add-allocation-non-empty": {
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Status.DriverName = "valid"
claim.Status.Allocation = &resource.AllocationResult{
ResourceHandles: []resource.ResourceHandle{
{
DriverName: "valid",
Data: strings.Repeat(" ", resource.ResourceHandleDataMaxSize),
},
Devices: resource.DeviceAllocationResult{
Results: []resource.DeviceRequestAllocationResult{{
Request: goodName,
Driver: goodName,
Pool: goodName,
Device: goodName,
}},
},
}
return claim
},
},
"valid-add-empty-allocation-structured": {
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Status.DriverName = "valid"
claim.Status.Allocation = &resource.AllocationResult{
ResourceHandles: []resource.ResourceHandle{
{
DriverName: "valid",
StructuredData: &resource.StructuredResourceHandle{},
},
},
}
return claim
},
},
"valid-add-allocation-structured": {
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Status.DriverName = "valid"
claim.Status.Allocation = &resource.AllocationResult{
ResourceHandles: []resource.ResourceHandle{
{
DriverName: "valid",
StructuredData: &resource.StructuredResourceHandle{
NodeName: "worker",
},
},
},
}
return claim
},
},
"invalid-add-allocation-structured": {
"invalid-add-allocation-bad-request": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("status", "allocation", "resourceHandles").Index(0).Child("structuredData", "nodeName"), "&^!", "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", "resourceHandles").Index(0).Child("structuredData", "results").Index(1), "exactly one structured model field must be set"),
field.Invalid(field.NewPath("status", "allocation", "devices", "results").Index(0).Child("request"), 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("status", "allocation", "devices", "results").Index(0).Child("request"), badName, "must be the name of a request in the claim"),
},
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Status.DriverName = "valid"
claim.Status.Allocation = &resource.AllocationResult{
ResourceHandles: []resource.ResourceHandle{
{
DriverName: "valid",
StructuredData: &resource.StructuredResourceHandle{
NodeName: "&^!",
Results: []resource.DriverAllocationResult{
{
AllocationResultModel: resource.AllocationResultModel{
NamedResources: &resource.NamedResourcesAllocationResult{
Name: "some-resource-instance",
},
},
},
{
AllocationResultModel: resource.AllocationResultModel{}, // invalid
},
},
},
},
},
}
return claim
},
},
"invalid-duplicated-data": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("status", "allocation", "resourceHandles").Index(0), nil, "data and structuredData are mutually exclusive")},
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Status.DriverName = "valid"
claim.Status.Allocation = &resource.AllocationResult{
ResourceHandles: []resource.ResourceHandle{
{
DriverName: "valid",
Data: "something",
StructuredData: &resource.StructuredResourceHandle{
NodeName: "worker",
},
},
},
}
return claim
},
},
"invalid-allocation-resourceHandles": {
wantFailures: field.ErrorList{field.TooLongMaxLength(field.NewPath("status", "allocation", "resourceHandles"), resource.AllocationResultResourceHandlesMaxSize+1, resource.AllocationResultResourceHandlesMaxSize)},
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Status.DriverName = "valid"
claim.Status.Allocation = &resource.AllocationResult{
ResourceHandles: func() []resource.ResourceHandle {
var handles []resource.ResourceHandle
for i := 0; i < resource.AllocationResultResourceHandlesMaxSize+1; i++ {
handles = append(handles, resource.ResourceHandle{DriverName: "valid"})
}
return handles
}(),
}
return claim
},
},
"invalid-allocation-resource-handle-drivername": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("status", "allocation", "resourceHandles[0]", "driverName"), invalidName, "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])?)*')")},
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Status.DriverName = "valid"
claim.Status.Allocation = &resource.AllocationResult{
ResourceHandles: []resource.ResourceHandle{
{
DriverName: invalidName,
},
},
}
return claim
},
},
"invalid-allocation-resource-handle-data": {
wantFailures: field.ErrorList{field.TooLongMaxLength(field.NewPath("status", "allocation", "resourceHandles").Index(0).Child("data"), resource.ResourceHandleDataMaxSize+1, resource.ResourceHandleDataMaxSize)},
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Status.DriverName = "valid"
claim.Status.Allocation = &resource.AllocationResult{
ResourceHandles: []resource.ResourceHandle{
{
DriverName: "valid",
Data: strings.Repeat(" ", resource.ResourceHandleDataMaxSize+1),
},
Devices: resource.DeviceAllocationResult{
Results: []resource.DeviceRequestAllocationResult{{
Request: badName,
Driver: goodName,
Pool: goodName,
Device: goodName,
}},
},
}
return claim
},
},
"invalid-node-selector": {
wantFailures: field.ErrorList{field.Required(field.NewPath("status", "allocation", "availableOnNodes", "nodeSelectorTerms"), "must have at least one node selector term")},
wantFailures: field.ErrorList{field.Required(field.NewPath("status", "allocation", "nodeSelector", "nodeSelectorTerms"), "must have at least one node selector term")},
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Status.DriverName = "valid"
claim.Status.Allocation = &resource.AllocationResult{
AvailableOnNodes: &core.NodeSelector{
NodeSelector: &core.NodeSelector{
// Must not be empty.
},
}
@@ -587,7 +501,6 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "may not be specified when `allocated` is not set")},
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Status.DriverName = "valid"
claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{
{
Resource: "pods",
@@ -708,26 +621,12 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
"invalid-allocation-modification": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("status.allocation"), func() *resource.AllocationResult {
claim := validAllocatedClaim.DeepCopy()
claim.Status.Allocation.ResourceHandles = []resource.ResourceHandle{
{
DriverName: "valid",
Data: strings.Repeat(" ", resource.ResourceHandleDataMaxSize/2),
},
}
claim.Status.Allocation.Devices.Results[0].Driver += "-2"
return claim.Status.Allocation
}(), "field is immutable")},
oldClaim: func() *resource.ResourceClaim {
claim := validAllocatedClaim.DeepCopy()
claim.Status.DeallocationRequested = false
return claim
}(),
oldClaim: validAllocatedClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Status.Allocation.ResourceHandles = []resource.ResourceHandle{
{
DriverName: "valid",
Data: strings.Repeat(" ", resource.ResourceHandleDataMaxSize/2),
},
}
claim.Status.Allocation.Devices.Results[0].Driver += "-2"
return claim
},
},
@@ -769,12 +668,36 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
return claim
},
},
"invalid-request-name": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("status", "allocation", "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("status", "allocation", "devices", "config").Index(0).Child("requests").Index(1), badName, "must be the name of a request in the claim"),
},
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,
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"}`),
},
},
},
}}
return claim
},
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
scenario.oldClaim.ResourceVersion = "1"
errs := ValidateClaimStatusUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim)
errs := ValidateResourceClaimStatusUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim)
assert.Equal(t, scenario.wantFailures, errs)
})
}

View File

@@ -1,306 +0,0 @@
/*
Copyright 2022 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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/utils/ptr"
)
func testResourceClaimParameters(name, namespace string, requests []resource.DriverRequests) *resource.ResourceClaimParameters {
return &resource.ResourceClaimParameters{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
DriverRequests: requests,
}
}
var goodRequests []resource.DriverRequests
func TestValidateResourceClaimParameters(t *testing.T) {
goodName := "foo"
badName := "!@#$%^"
badValue := "spaces not allowed"
now := metav1.Now()
scenarios := map[string]struct {
parameters *resource.ResourceClaimParameters
wantFailures field.ErrorList
}{
"good": {
parameters: testResourceClaimParameters(goodName, goodName, goodRequests),
},
"missing-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
parameters: testResourceClaimParameters("", goodName, goodRequests),
},
"missing-namespace": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
parameters: testResourceClaimParameters(goodName, "", goodRequests),
},
"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])?)*')")},
parameters: testResourceClaimParameters(badName, goodName, goodRequests),
},
"bad-namespace": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), 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])?')")},
parameters: testResourceClaimParameters(goodName, badName, goodRequests),
},
"generate-name": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.GenerateName = "prefix-"
return parameters
}(),
},
"uid": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
return parameters
}(),
},
"resource-version": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.ResourceVersion = "1"
return parameters
}(),
},
"generation": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.Generation = 100
return parameters
}(),
},
"creation-timestamp": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.CreationTimestamp = now
return parameters
}(),
},
"deletion-grace-period-seconds": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.DeletionGracePeriodSeconds = ptr.To[int64](10)
return parameters
}(),
},
"owner-references": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "pod",
Name: "foo",
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
},
}
return parameters
}(),
},
"finalizers": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.Finalizers = []string{
"example.com/foo",
}
return parameters
}(),
},
"managed-fields": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.ManagedFields = []metav1.ManagedFieldsEntry{
{
FieldsType: "FieldsV1",
Operation: "Apply",
APIVersion: "apps/v1",
Manager: "foo",
},
}
return parameters
}(),
},
"good-labels": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.Labels = map[string]string{
"apps.kubernetes.io/name": "test",
}
return parameters
}(),
},
"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])?')")},
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.Labels = map[string]string{
"hello-world": badValue,
}
return parameters
}(),
},
"good-annotations": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.Annotations = map[string]string{
"foo": "bar",
}
return parameters
}(),
},
"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]')")},
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.Annotations = map[string]string{
badName: "hello world",
}
return parameters
}(),
},
"empty-model": {
wantFailures: field.ErrorList{field.Required(field.NewPath("driverRequests").Index(0).Child("requests").Index(0), "exactly one structured model field must be set")},
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.DriverRequests = []resource.DriverRequests{{DriverName: goodName, Requests: []resource.ResourceRequest{{}}}}
return parameters
}(),
},
"empty-requests": {
wantFailures: field.ErrorList{field.Required(field.NewPath("driverRequests").Index(0).Child("requests"), "empty entries with no requests are not allowed")},
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.DriverRequests = []resource.DriverRequests{{DriverName: goodName}}
return parameters
}(),
},
"invalid-driver": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverRequests").Index(1).Child("driverName"), 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])?)*')")},
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.DriverRequests = []resource.DriverRequests{
{
DriverName: goodName,
Requests: []resource.ResourceRequest{
{
ResourceRequestModel: resource.ResourceRequestModel{
NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
},
},
},
},
{
DriverName: badName,
Requests: []resource.ResourceRequest{
{
ResourceRequestModel: resource.ResourceRequestModel{
NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
},
},
},
},
}
return parameters
}(),
},
"duplicate-driver": {
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("driverRequests").Index(1).Child("driverName"), goodName)},
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.DriverRequests = []resource.DriverRequests{
{
DriverName: goodName,
Requests: []resource.ResourceRequest{
{
ResourceRequestModel: resource.ResourceRequestModel{
NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
},
},
},
},
{
DriverName: goodName,
Requests: []resource.ResourceRequest{
{
ResourceRequestModel: resource.ResourceRequestModel{
NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
},
},
},
},
}
return parameters
}(),
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
errs := ValidateResourceClaimParameters(scenario.parameters)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}
func TestValidateResourceClaimParametersUpdate(t *testing.T) {
name := "valid"
validResourceClaimParameters := testResourceClaimParameters(name, name, nil)
scenarios := map[string]struct {
oldResourceClaimParameters *resource.ResourceClaimParameters
update func(claim *resource.ResourceClaimParameters) *resource.ResourceClaimParameters
wantFailures field.ErrorList
}{
"valid-no-op-update": {
oldResourceClaimParameters: validResourceClaimParameters,
update: func(claim *resource.ResourceClaimParameters) *resource.ResourceClaimParameters { return claim },
},
"invalid-name-update": {
oldResourceClaimParameters: validResourceClaimParameters,
update: func(claim *resource.ResourceClaimParameters) *resource.ResourceClaimParameters {
claim.Name += "-update"
return claim
},
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
scenario.oldResourceClaimParameters.ResourceVersion = "1"
errs := ValidateResourceClaimParametersUpdate(scenario.update(scenario.oldResourceClaimParameters.DeepCopy()), scenario.oldResourceClaimParameters)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}

View File

@@ -34,87 +34,79 @@ func testClaimTemplate(name, namespace string, spec resource.ResourceClaimSpec)
Namespace: namespace,
},
Spec: resource.ResourceClaimTemplateSpec{
Spec: spec,
Spec: *spec.DeepCopy(),
},
}
}
func TestValidateClaimTemplate(t *testing.T) {
goodName := "foo"
badName := "!@#$%^"
goodNS := "ns"
goodClaimSpec := resource.ResourceClaimSpec{
ResourceClassName: goodName,
}
now := metav1.Now()
badValue := "spaces not allowed"
badAPIGroup := "example.com/v1"
goodAPIGroup := "example.com"
scenarios := map[string]struct {
template *resource.ResourceClaimTemplate
wantFailures field.ErrorList
}{
"good-claim": {
template: testClaimTemplate(goodName, goodNS, goodClaimSpec),
template: testClaimTemplate(goodName, goodNS, validClaimSpec),
},
"missing-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
template: testClaimTemplate("", goodNS, goodClaimSpec),
template: testClaimTemplate("", goodNS, validClaimSpec),
},
"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])?)*')")},
template: testClaimTemplate(badName, goodNS, goodClaimSpec),
template: testClaimTemplate(badName, goodNS, validClaimSpec),
},
"missing-namespace": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
template: testClaimTemplate(goodName, "", goodClaimSpec),
template: testClaimTemplate(goodName, "", validClaimSpec),
},
"generate-name": {
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.GenerateName = "pvc-"
return template
}(),
},
"uid": {
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
return template
}(),
},
"resource-version": {
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.ResourceVersion = "1"
return template
}(),
},
"generation": {
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.Generation = 100
return template
}(),
},
"creation-timestamp": {
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.CreationTimestamp = now
return template
}(),
},
"deletion-grace-period-seconds": {
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.DeletionGracePeriodSeconds = pointer.Int64(10)
return template
}(),
},
"owner-references": {
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "v1",
@@ -128,7 +120,7 @@ func TestValidateClaimTemplate(t *testing.T) {
},
"finalizers": {
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.Finalizers = []string{
"example.com/foo",
}
@@ -137,7 +129,7 @@ func TestValidateClaimTemplate(t *testing.T) {
},
"managed-fields": {
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.ManagedFields = []metav1.ManagedFieldsEntry{
{
FieldsType: "FieldsV1",
@@ -151,7 +143,7 @@ func TestValidateClaimTemplate(t *testing.T) {
},
"good-labels": {
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.Labels = map[string]string{
"apps.kubernetes.io/name": "test",
}
@@ -161,7 +153,7 @@ func TestValidateClaimTemplate(t *testing.T) {
"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])?')")},
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.Labels = map[string]string{
"hello-world": badValue,
}
@@ -170,7 +162,7 @@ func TestValidateClaimTemplate(t *testing.T) {
},
"good-annotations": {
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.Annotations = map[string]string{
"foo": "bar",
}
@@ -180,7 +172,7 @@ func TestValidateClaimTemplate(t *testing.T) {
"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]')")},
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.Annotations = map[string]string{
badName: "hello world",
}
@@ -188,63 +180,10 @@ func TestValidateClaimTemplate(t *testing.T) {
}(),
},
"bad-classname": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "spec", "resourceClassName"), 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", "spec", "devices", "requests").Index(0).Child("deviceClassName"), 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])?)*')")},
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template.Spec.Spec.ResourceClassName = badName
return template
}(),
},
"good-parameters": {
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
Kind: "foo",
Name: "bar",
}
return template
}(),
},
"good-parameters-apigroup": {
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
APIGroup: goodAPIGroup,
Kind: "foo",
Name: "bar",
}
return template
}(),
},
"bad-parameters-apigroup": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "spec", "parametersRef", "apiGroup"), badAPIGroup, "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])?)*')")},
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
APIGroup: badAPIGroup,
Kind: "foo",
Name: "bar",
}
return template
}(),
},
"missing-parameters-kind": {
wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "spec", "parametersRef", "kind"), "")},
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
Name: "bar",
}
return template
}(),
},
"missing-parameters-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "spec", "parametersRef", "name"), "")},
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
Kind: "foo",
}
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.Spec.Spec.Devices.Requests[0].DeviceClassName = badName
return template
}(),
},
@@ -252,22 +191,14 @@ func TestValidateClaimTemplate(t *testing.T) {
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
errs := ValidateClaimTemplate(scenario.template)
errs := ValidateResourceClaimTemplate(scenario.template)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}
func TestValidateClaimTemplateUpdate(t *testing.T) {
name := "valid"
parameters := &resource.ResourceClaimParametersReference{
Kind: "foo",
Name: "bar",
}
validClaimTemplate := testClaimTemplate("foo", "ns", resource.ResourceClaimSpec{
ResourceClassName: name,
ParametersRef: parameters,
})
validClaimTemplate := testClaimTemplate(goodName, goodNS, validClaimSpec)
scenarios := map[string]struct {
oldClaimTemplate *resource.ResourceClaimTemplate
@@ -281,24 +212,12 @@ func TestValidateClaimTemplateUpdate(t *testing.T) {
"invalid-update-class": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimTemplateSpec {
spec := validClaimTemplate.Spec.DeepCopy()
spec.Spec.ResourceClassName += "2"
spec.Spec.Devices.Requests[0].DeviceClassName += "2"
return *spec
}(), "field is immutable")},
oldClaimTemplate: validClaimTemplate,
update: func(template *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate {
template.Spec.Spec.ResourceClassName += "2"
return template
},
},
"invalid-update-remove-parameters": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimTemplateSpec {
spec := validClaimTemplate.Spec.DeepCopy()
spec.Spec.ParametersRef = nil
return *spec
}(), "field is immutable")},
oldClaimTemplate: validClaimTemplate,
update: func(template *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate {
template.Spec.Spec.ParametersRef = nil
template.Spec.Spec.Devices.Requests[0].DeviceClassName += "2"
return template
},
},
@@ -307,7 +226,7 @@ func TestValidateClaimTemplateUpdate(t *testing.T) {
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
scenario.oldClaimTemplate.ResourceVersion = "1"
errs := ValidateClaimTemplateUpdate(scenario.update(scenario.oldClaimTemplate.DeepCopy()), scenario.oldClaimTemplate)
errs := ValidateResourceClaimTemplateUpdate(scenario.update(scenario.oldClaimTemplate.DeepCopy()), scenario.oldClaimTemplate)
assert.Equal(t, scenario.wantFailures, errs)
})
}

View File

@@ -1,301 +0,0 @@
/*
Copyright 2022 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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/utils/pointer"
)
func testClass(name, driverName string) *resource.ResourceClass {
return &resource.ResourceClass{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
DriverName: driverName,
}
}
func TestValidateClass(t *testing.T) {
goodName := "foo"
now := metav1.Now()
goodParameters := resource.ResourceClassParametersReference{
Name: "valid",
Namespace: "valid",
Kind: "foo",
}
badName := "!@#$%^"
badValue := "spaces not allowed"
badAPIGroup := "example.com/v1"
goodAPIGroup := "example.com"
scenarios := map[string]struct {
class *resource.ResourceClass
wantFailures field.ErrorList
}{
"good-class": {
class: testClass(goodName, goodName),
},
"good-long-driver-name": {
class: testClass(goodName, "acme.example.com"),
},
"missing-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
class: testClass("", goodName),
},
"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])?)*')")},
class: testClass(badName, goodName),
},
"generate-name": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.GenerateName = "pvc-"
return class
}(),
},
"uid": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
return class
}(),
},
"resource-version": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ResourceVersion = "1"
return class
}(),
},
"generation": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.Generation = 100
return class
}(),
},
"creation-timestamp": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.CreationTimestamp = now
return class
}(),
},
"deletion-grace-period-seconds": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.DeletionGracePeriodSeconds = pointer.Int64(10)
return class
}(),
},
"owner-references": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "pod",
Name: "foo",
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
},
}
return class
}(),
},
"finalizers": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.Finalizers = []string{
"example.com/foo",
}
return class
}(),
},
"managed-fields": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ManagedFields = []metav1.ManagedFieldsEntry{
{
FieldsType: "FieldsV1",
Operation: "Apply",
APIVersion: "apps/v1",
Manager: "foo",
},
}
return class
}(),
},
"good-labels": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.Labels = map[string]string{
"apps.kubernetes.io/name": "test",
}
return class
}(),
},
"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])?')")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.Labels = map[string]string{
"hello-world": badValue,
}
return class
}(),
},
"good-annotations": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.Annotations = map[string]string{
"foo": "bar",
}
return class
}(),
},
"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]')")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.Annotations = map[string]string{
badName: "hello world",
}
return class
}(),
},
"missing-driver-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("driverName"), ""),
field.Invalid(field.NewPath("driverName"), "", "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])?)*')"),
},
class: testClass(goodName, ""),
},
"invalid-driver-name": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), 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])?)*')")},
class: testClass(goodName, badName),
},
"invalid-qualified-driver-name": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), goodName+"/path", "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])?)*')")},
class: testClass(goodName, goodName+"/path"),
},
"good-parameters": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ParametersRef = goodParameters.DeepCopy()
return class
}(),
},
"good-parameters-apigroup": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ParametersRef = goodParameters.DeepCopy()
class.ParametersRef.APIGroup = goodAPIGroup
return class
}(),
},
"bad-parameters-apigroup": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("parametersRef", "apiGroup"), badAPIGroup, "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])?)*')")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ParametersRef = goodParameters.DeepCopy()
class.ParametersRef.APIGroup = badAPIGroup
return class
}(),
},
"missing-parameters-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("parametersRef", "name"), "")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ParametersRef = goodParameters.DeepCopy()
class.ParametersRef.Name = ""
return class
}(),
},
"bad-parameters-namespace": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("parametersRef", "namespace"), 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])?')")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ParametersRef = goodParameters.DeepCopy()
class.ParametersRef.Namespace = badName
return class
}(),
},
"missing-parameters-kind": {
wantFailures: field.ErrorList{field.Required(field.NewPath("parametersRef", "kind"), "")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ParametersRef = goodParameters.DeepCopy()
class.ParametersRef.Kind = ""
return class
}(),
},
"invalid-node-selector": {
wantFailures: field.ErrorList{field.Required(field.NewPath("suitableNodes", "nodeSelectorTerms"), "must have at least one node selector term")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.SuitableNodes = &core.NodeSelector{
// Must not be empty.
}
return class
}(),
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
errs := ValidateClass(scenario.class)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}
func TestValidateClassUpdate(t *testing.T) {
validClass := testClass("foo", "valid")
scenarios := map[string]struct {
oldClass *resource.ResourceClass
update func(class *resource.ResourceClass) *resource.ResourceClass
wantFailures field.ErrorList
}{
"valid-no-op-update": {
oldClass: validClass,
update: func(class *resource.ResourceClass) *resource.ResourceClass { return class },
},
"update-driver": {
oldClass: validClass,
update: func(class *resource.ResourceClass) *resource.ResourceClass {
class.DriverName += "2"
return class
},
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
scenario.oldClass.ResourceVersion = "1"
errs := ValidateClassUpdate(scenario.update(scenario.oldClass.DeepCopy()), scenario.oldClass)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}

View File

@@ -1,313 +0,0 @@
/*
Copyright 2022 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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/utils/ptr"
)
func testResourceClassParameters(name, namespace string, filters []resource.ResourceFilter) *resource.ResourceClassParameters {
return &resource.ResourceClassParameters{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Filters: filters,
}
}
var goodFilters []resource.ResourceFilter
func TestValidateResourceClassParameters(t *testing.T) {
goodName := "foo"
badName := "!@#$%^"
badValue := "spaces not allowed"
now := metav1.Now()
scenarios := map[string]struct {
parameters *resource.ResourceClassParameters
wantFailures field.ErrorList
}{
"good": {
parameters: testResourceClassParameters(goodName, goodName, goodFilters),
},
"missing-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
parameters: testResourceClassParameters("", goodName, goodFilters),
},
"missing-namespace": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
parameters: testResourceClassParameters(goodName, "", goodFilters),
},
"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])?)*')")},
parameters: testResourceClassParameters(badName, goodName, goodFilters),
},
"bad-namespace": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), 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])?')")},
parameters: testResourceClassParameters(goodName, badName, goodFilters),
},
"generate-name": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.GenerateName = "prefix-"
return parameters
}(),
},
"uid": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
return parameters
}(),
},
"resource-version": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.ResourceVersion = "1"
return parameters
}(),
},
"generation": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Generation = 100
return parameters
}(),
},
"creation-timestamp": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.CreationTimestamp = now
return parameters
}(),
},
"deletion-grace-period-seconds": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.DeletionGracePeriodSeconds = ptr.To[int64](10)
return parameters
}(),
},
"owner-references": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "pod",
Name: "foo",
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
},
}
return parameters
}(),
},
"finalizers": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Finalizers = []string{
"example.com/foo",
}
return parameters
}(),
},
"managed-fields": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.ManagedFields = []metav1.ManagedFieldsEntry{
{
FieldsType: "FieldsV1",
Operation: "Apply",
APIVersion: "apps/v1",
Manager: "foo",
},
}
return parameters
}(),
},
"good-labels": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Labels = map[string]string{
"apps.kubernetes.io/name": "test",
}
return parameters
}(),
},
"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])?')")},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Labels = map[string]string{
"hello-world": badValue,
}
return parameters
}(),
},
"good-annotations": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Annotations = map[string]string{
"foo": "bar",
}
return parameters
}(),
},
"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]')")},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Annotations = map[string]string{
badName: "hello world",
}
return parameters
}(),
},
"empty-model": {
wantFailures: field.ErrorList{field.Required(field.NewPath("filters").Index(0), "exactly one structured model field must be set")},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Filters = []resource.ResourceFilter{{DriverName: goodName}}
return parameters
}(),
},
"filters-invalid-driver": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("filters").Index(1).Child("driverName"), 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])?)*')")},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Filters = []resource.ResourceFilter{
{
DriverName: goodName,
ResourceFilterModel: resource.ResourceFilterModel{
NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
},
},
{
DriverName: badName,
ResourceFilterModel: resource.ResourceFilterModel{
NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
},
},
}
return parameters
}(),
},
"filters-duplicate-driver": {
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("filters").Index(1).Child("driverName"), goodName)},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Filters = []resource.ResourceFilter{
{
DriverName: goodName,
ResourceFilterModel: resource.ResourceFilterModel{
NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
},
},
{
DriverName: goodName,
ResourceFilterModel: resource.ResourceFilterModel{
NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
},
},
}
return parameters
}(),
},
"parameters-invalid-driver": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("vendorParameters").Index(1).Child("driverName"), 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])?)*')")},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.VendorParameters = []resource.VendorParameters{
{
DriverName: goodName,
},
{
DriverName: badName,
},
}
return parameters
}(),
},
"parameters-duplicate-driver": {
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("vendorParameters").Index(1).Child("driverName"), goodName)},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.VendorParameters = []resource.VendorParameters{
{
DriverName: goodName,
},
{
DriverName: goodName,
},
}
return parameters
}(),
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
errs := ValidateResourceClassParameters(scenario.parameters)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}
func TestValidateResourceClassParametersUpdate(t *testing.T) {
name := "valid"
validResourceClassParameters := testResourceClassParameters(name, name, nil)
scenarios := map[string]struct {
oldResourceClassParameters *resource.ResourceClassParameters
update func(class *resource.ResourceClassParameters) *resource.ResourceClassParameters
wantFailures field.ErrorList
}{
"valid-no-op-update": {
oldResourceClassParameters: validResourceClassParameters,
update: func(class *resource.ResourceClassParameters) *resource.ResourceClassParameters { return class },
},
"invalid-name-update": {
oldResourceClassParameters: validResourceClassParameters,
update: func(class *resource.ResourceClassParameters) *resource.ResourceClassParameters {
class.Name += "-update"
return class
},
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
scenario.oldResourceClassParameters.ResourceVersion = "1"
errs := ValidateResourceClassParametersUpdate(scenario.update(scenario.oldResourceClassParameters.DeepCopy()), scenario.oldResourceClassParameters)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}

View File

@@ -32,10 +32,13 @@ func testResourceSlice(name, nodeName, driverName string) *resource.ResourceSlic
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
NodeName: nodeName,
DriverName: driverName,
ResourceModel: resource.ResourceModel{
NamedResources: &resource.NamedResourcesResources{},
Spec: resource.ResourceSliceSpec{
NodeName: nodeName,
Driver: driverName,
Pool: resource.ResourcePool{
Name: nodeName,
ResourceSliceCount: 1,
},
},
}
}
@@ -180,22 +183,24 @@ func TestValidateResourceSlice(t *testing.T) {
}(),
},
"bad-nodename": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("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),
wantFailures: field.ErrorList{
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])?)*')"),
},
slice: testResourceSlice(goodName, badName, driverName),
},
"bad-multi-pool-name": {
wantFailures: field.ErrorList{
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])?)*')"),
},
slice: testResourceSlice(goodName, badName+"/"+badName, driverName),
},
"bad-drivername": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), 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),
},
"empty-model": {
wantFailures: field.ErrorList{field.Required(nil, "exactly one structured model field must be set")},
slice: func() *resource.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName)
slice.ResourceModel = resource.ResourceModel{}
return slice
}(),
},
}
for name, scenario := range scenarios {
@@ -228,18 +233,26 @@ func TestValidateResourceSliceUpdate(t *testing.T) {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
},
"invalid-update-nodename": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("nodeName"), name+"-updated", "field is immutable")},
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "nodeName"), name+"-updated", "field is immutable")},
oldResourceSlice: validResourceSlice,
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
slice.NodeName += "-updated"
slice.Spec.NodeName += "-updated"
return slice
},
},
"invalid-update-drivername": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), name+"-updated", "field is immutable")},
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "driver"), name+"-updated", "field is immutable")},
oldResourceSlice: validResourceSlice,
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
slice.DriverName += "-updated"
slice.Spec.Driver += "-updated"
return slice
},
},
"invalid-update-pool": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "pool", "name"), validResourceSlice.Spec.Pool.Name+"-updated", "field is immutable")},
oldResourceSlice: validResourceSlice,
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
slice.Spec.Pool.Name += "-updated"
return slice
},
},

File diff suppressed because it is too large Load Diff