@@ -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.
|
||||
|
||||
2
pkg/apis/core/v1/zz_generated.conversion.go
generated
2
pkg/apis/core/v1/zz_generated.conversion.go
generated
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -18,23 +18,43 @@ 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 []interface{}{
|
||||
func(obj *resource.ResourceClaimSpec, c fuzz.Continue) {
|
||||
c.FuzzNoCustom(obj) // fuzz self without calling this function again
|
||||
func(r *resource.DeviceRequest, c fuzz.Continue) {
|
||||
c.FuzzNoCustom(r) // fuzz self without calling this function again
|
||||
|
||||
// Custom fuzzing for allocation mode: pick one valid mode randomly.
|
||||
modes := []resource.AllocationMode{
|
||||
resource.AllocationModeImmediate,
|
||||
resource.AllocationModeWaitForFirstConsumer,
|
||||
if r.AllocationMode == "" {
|
||||
r.AllocationMode = []resource.DeviceAllocationMode{
|
||||
resource.DeviceAllocationModeAll,
|
||||
resource.DeviceAllocationModeExactCount,
|
||||
}[c.Int31n(2)]
|
||||
}
|
||||
obj.AllocationMode = modes[c.Rand.Intn(len(modes))]
|
||||
},
|
||||
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"}`)}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
"k8s.io/kubernetes/pkg/apis/resource"
|
||||
"k8s.io/kubernetes/pkg/apis/resource/v1alpha2"
|
||||
"k8s.io/kubernetes/pkg/apis/resource/v1alpha3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -33,6 +33,6 @@ func init() {
|
||||
// Install registers the API group and adds types to a scheme
|
||||
func Install(scheme *runtime.Scheme) {
|
||||
utilruntime.Must(resource.AddToScheme(scheme))
|
||||
utilruntime.Must(v1alpha2.AddToScheme(scheme))
|
||||
utilruntime.Must(scheme.SetVersionPriority(v1alpha2.SchemeGroupVersion))
|
||||
utilruntime.Must(v1alpha3.AddToScheme(scheme))
|
||||
utilruntime.Must(scheme.SetVersionPriority(v1alpha3.SchemeGroupVersion))
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestResourceVersioner(t *testing.T) {
|
||||
|
||||
func TestCodec(t *testing.T) {
|
||||
claim := internal.ResourceClaim{}
|
||||
data, err := runtime.Encode(legacyscheme.Codecs.LegacyCodec(schema.GroupVersion{Group: "resource.k8s.io", Version: "v1alpha2"}), &claim)
|
||||
data, err := runtime.Encode(legacyscheme.Codecs.LegacyCodec(schema.GroupVersion{Group: "resource.k8s.io", Version: "v1alpha3"}), &claim)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func TestCodec(t *testing.T) {
|
||||
if err := json.Unmarshal(data, &other); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if other.APIVersion != "resource.k8s.io/v1alpha2" || other.Kind != "ResourceClaim" {
|
||||
if other.APIVersion != "resource.k8s.io/v1alpha3" || other.Kind != "ResourceClaim" {
|
||||
t.Errorf("unexpected unmarshalled object %#v", other)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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
1749
pkg/apis/resource/v1alpha2/zz_generated.conversion.go
generated
1749
pkg/apis/resource/v1alpha2/zz_generated.conversion.go
generated
File diff suppressed because it is too large
Load Diff
@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha2
|
||||
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)
|
||||
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha2
|
||||
package v1alpha3
|
||||
|
||||
import (
|
||||
"k8s.io/api/resource/v1alpha2"
|
||||
resourceapi "k8s.io/api/resource/v1alpha3"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
@@ -25,8 +25,12 @@ func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
||||
return RegisterDefaults(scheme)
|
||||
}
|
||||
|
||||
func SetDefaults_ResourceClaimSpec(obj *v1alpha2.ResourceClaimSpec) {
|
||||
func SetDefaults_DeviceRequest(obj *resourceapi.DeviceRequest) {
|
||||
if obj.AllocationMode == "" {
|
||||
obj.AllocationMode = v1alpha2.AllocationModeWaitForFirstConsumer
|
||||
obj.AllocationMode = resourceapi.DeviceAllocationModeExactCount
|
||||
}
|
||||
|
||||
if obj.AllocationMode == resourceapi.DeviceAllocationModeExactCount && obj.Count == 0 {
|
||||
obj.Count = 1
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha2_test
|
||||
package v1alpha3_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
v1alpha2 "k8s.io/api/resource/v1alpha2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
v1alpha3 "k8s.io/api/resource/v1alpha3"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
// ensure types are installed
|
||||
@@ -29,32 +31,41 @@ import (
|
||||
)
|
||||
|
||||
func TestSetDefaultAllocationMode(t *testing.T) {
|
||||
claim := &v1alpha2.ResourceClaim{}
|
||||
|
||||
// field should be defaulted
|
||||
defaultMode := v1alpha2.AllocationModeWaitForFirstConsumer
|
||||
output := roundTrip(t, runtime.Object(claim)).(*v1alpha2.ResourceClaim)
|
||||
outMode := output.Spec.AllocationMode
|
||||
if outMode != defaultMode {
|
||||
t.Errorf("Expected AllocationMode to be defaulted to: %+v, got: %+v", defaultMode, outMode)
|
||||
}
|
||||
|
||||
// field should not change
|
||||
nonDefaultMode := v1alpha2.AllocationModeImmediate
|
||||
claim = &v1alpha2.ResourceClaim{
|
||||
Spec: v1alpha2.ResourceClaimSpec{
|
||||
AllocationMode: nonDefaultMode,
|
||||
claim := &v1alpha3.ResourceClaim{
|
||||
Spec: v1alpha3.ResourceClaimSpec{
|
||||
Devices: v1alpha3.DeviceClaim{
|
||||
Requests: []v1alpha3.DeviceRequest{{}},
|
||||
},
|
||||
},
|
||||
}
|
||||
output = roundTrip(t, runtime.Object(claim)).(*v1alpha2.ResourceClaim)
|
||||
outMode = output.Spec.AllocationMode
|
||||
if outMode != v1alpha2.AllocationModeImmediate {
|
||||
t.Errorf("Expected AllocationMode to remain %+v, got: %+v", nonDefaultMode, outMode)
|
||||
|
||||
// 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(v1alpha2.SchemeGroupVersion)
|
||||
codec := legacyscheme.Codecs.LegacyCodec(v1alpha3.SchemeGroupVersion)
|
||||
data, err := runtime.Encode(codec, obj)
|
||||
if err != nil {
|
||||
t.Errorf("%v\n %#v", err, obj)
|
||||
@@ -15,9 +15,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// +k8s:conversion-gen=k8s.io/kubernetes/pkg/apis/resource
|
||||
// +k8s:conversion-gen-external-types=k8s.io/api/resource/v1alpha2
|
||||
// +k8s:conversion-gen-external-types=k8s.io/api/resource/v1alpha3
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +k8s:defaulter-gen-input=k8s.io/api/resource/v1alpha2
|
||||
// +k8s:defaulter-gen-input=k8s.io/api/resource/v1alpha3
|
||||
|
||||
// Package v1alpha2 is the v1alpha2 version of the resource API.
|
||||
package v1alpha2 // import "k8s.io/kubernetes/pkg/apis/resource/v1alpha2"
|
||||
// Package v1alpha3 is the v1alpha3 version of the resource API.
|
||||
package v1alpha3 // import "k8s.io/kubernetes/pkg/apis/resource/v1alpha3"
|
||||
@@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha2
|
||||
package v1alpha3
|
||||
|
||||
import (
|
||||
"k8s.io/api/resource/v1alpha2"
|
||||
"k8s.io/api/resource/v1alpha3"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
localSchemeBuilder = &v1alpha2.SchemeBuilder
|
||||
localSchemeBuilder = &v1alpha3.SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ func init() {
|
||||
const GroupName = "resource.k8s.io"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha2"}
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha3"}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
1287
pkg/apis/resource/v1alpha3/zz_generated.conversion.go
generated
Normal file
1287
pkg/apis/resource/v1alpha3/zz_generated.conversion.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,10 +19,10 @@ limitations under the License.
|
||||
|
||||
// Code generated by defaulter-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha2
|
||||
package v1alpha3
|
||||
|
||||
import (
|
||||
v1alpha2 "k8s.io/api/resource/v1alpha2"
|
||||
v1alpha3 "k8s.io/api/resource/v1alpha3"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
@@ -30,31 +30,37 @@ 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(&v1alpha2.ResourceClaim{}, func(obj interface{}) { SetObjectDefaults_ResourceClaim(obj.(*v1alpha2.ResourceClaim)) })
|
||||
scheme.AddTypeDefaultingFunc(&v1alpha2.ResourceClaimList{}, func(obj interface{}) { SetObjectDefaults_ResourceClaimList(obj.(*v1alpha2.ResourceClaimList)) })
|
||||
scheme.AddTypeDefaultingFunc(&v1alpha2.ResourceClaimTemplate{}, func(obj interface{}) { SetObjectDefaults_ResourceClaimTemplate(obj.(*v1alpha2.ResourceClaimTemplate)) })
|
||||
scheme.AddTypeDefaultingFunc(&v1alpha2.ResourceClaimTemplateList{}, func(obj interface{}) {
|
||||
SetObjectDefaults_ResourceClaimTemplateList(obj.(*v1alpha2.ResourceClaimTemplateList))
|
||||
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 *v1alpha2.ResourceClaim) {
|
||||
SetDefaults_ResourceClaimSpec(&in.Spec)
|
||||
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 *v1alpha2.ResourceClaimList) {
|
||||
func SetObjectDefaults_ResourceClaimList(in *v1alpha3.ResourceClaimList) {
|
||||
for i := range in.Items {
|
||||
a := &in.Items[i]
|
||||
SetObjectDefaults_ResourceClaim(a)
|
||||
}
|
||||
}
|
||||
|
||||
func SetObjectDefaults_ResourceClaimTemplate(in *v1alpha2.ResourceClaimTemplate) {
|
||||
SetDefaults_ResourceClaimSpec(&in.Spec.Spec)
|
||||
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 *v1alpha2.ResourceClaimTemplateList) {
|
||||
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
247
pkg/apis/resource/validation/validation_deviceclass_test.go
Normal file
247
pkg/apis/resource/validation/validation_deviceclass_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,89 +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) {
|
||||
validMode := resource.AllocationModeImmediate
|
||||
invalidMode := resource.AllocationMode("invalid")
|
||||
goodName := "foo"
|
||||
badName := "!@#$%^"
|
||||
goodNS := "ns"
|
||||
goodClaimSpec := resource.ResourceClaimSpec{
|
||||
ResourceClassName: goodName,
|
||||
AllocationMode: validMode,
|
||||
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",
|
||||
@@ -133,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",
|
||||
}
|
||||
@@ -142,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",
|
||||
@@ -156,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",
|
||||
}
|
||||
@@ -166,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,
|
||||
}
|
||||
@@ -175,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",
|
||||
}
|
||||
@@ -185,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",
|
||||
}
|
||||
@@ -193,70 +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
|
||||
}(),
|
||||
},
|
||||
"bad-mode": {
|
||||
wantFailures: field.ErrorList{field.NotSupported(field.NewPath("spec", "allocationMode"), invalidMode, supportedAllocationModes.List())},
|
||||
"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.AllocationMode = invalidMode
|
||||
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
|
||||
}(),
|
||||
},
|
||||
"good-parameters": {
|
||||
"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, goodClaimSpec)
|
||||
claim.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
|
||||
Kind: "foo",
|
||||
Name: "bar",
|
||||
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
|
||||
}(),
|
||||
@@ -265,24 +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,
|
||||
AllocationMode: resource.AllocationModeImmediate,
|
||||
ParametersRef: parameters,
|
||||
})
|
||||
|
||||
scenarios := map[string]struct {
|
||||
oldClaim *resource.ResourceClaim
|
||||
update func(claim *resource.ResourceClaim) *resource.ResourceClaim
|
||||
@@ -295,36 +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
|
||||
return claim
|
||||
},
|
||||
},
|
||||
"invalid-update-mode": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimSpec {
|
||||
spec := validClaim.Spec.DeepCopy()
|
||||
spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer
|
||||
return *spec
|
||||
}(), "field is immutable")},
|
||||
oldClaim: validClaim,
|
||||
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||
claim.Spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer
|
||||
claim.Spec.Devices.Requests[0].DeviceClassName += "2"
|
||||
return claim
|
||||
},
|
||||
},
|
||||
@@ -333,35 +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",
|
||||
AllocationMode: resource.AllocationModeImmediate,
|
||||
})
|
||||
|
||||
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
|
||||
}(),
|
||||
Shareable: true,
|
||||
Devices: resource.DeviceAllocationResult{
|
||||
Results: []resource.DeviceRequestAllocationResult{{
|
||||
Request: goodName,
|
||||
Driver: goodName,
|
||||
Pool: goodName,
|
||||
Device: goodName,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -374,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.
|
||||
},
|
||||
}
|
||||
@@ -609,30 +497,10 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
|
||||
return claim
|
||||
},
|
||||
},
|
||||
"invalid-reserved-for-not-shared": {
|
||||
wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "may not be reserved more than once")},
|
||||
oldClaim: func() *resource.ResourceClaim {
|
||||
claim := validAllocatedClaim.DeepCopy()
|
||||
claim.Status.Allocation.Shareable = false
|
||||
return claim
|
||||
}(),
|
||||
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||
for i := 0; i < 2; i++ {
|
||||
claim.Status.ReservedFor = append(claim.Status.ReservedFor,
|
||||
resource.ResourceClaimConsumerReference{
|
||||
Resource: "pods",
|
||||
Name: fmt.Sprintf("foo-%d", i),
|
||||
UID: types.UID(fmt.Sprintf("%d", i)),
|
||||
})
|
||||
}
|
||||
return claim
|
||||
},
|
||||
},
|
||||
"invalid-reserved-for-no-allocation": {
|
||||
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",
|
||||
@@ -753,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
|
||||
},
|
||||
},
|
||||
@@ -814,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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -34,90 +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) {
|
||||
validMode := resource.AllocationModeImmediate
|
||||
invalidMode := resource.AllocationMode("invalid")
|
||||
goodName := "foo"
|
||||
badName := "!@#$%^"
|
||||
goodNS := "ns"
|
||||
goodClaimSpec := resource.ResourceClaimSpec{
|
||||
ResourceClassName: goodName,
|
||||
AllocationMode: validMode,
|
||||
}
|
||||
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",
|
||||
@@ -131,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",
|
||||
}
|
||||
@@ -140,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",
|
||||
@@ -154,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",
|
||||
}
|
||||
@@ -164,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,
|
||||
}
|
||||
@@ -173,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",
|
||||
}
|
||||
@@ -183,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",
|
||||
}
|
||||
@@ -191,71 +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
|
||||
}(),
|
||||
},
|
||||
"bad-mode": {
|
||||
wantFailures: field.ErrorList{field.NotSupported(field.NewPath("spec", "spec", "allocationMode"), invalidMode, supportedAllocationModes.List())},
|
||||
template: func() *resource.ResourceClaimTemplate {
|
||||
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||
template.Spec.Spec.AllocationMode = invalidMode
|
||||
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
|
||||
}(),
|
||||
},
|
||||
@@ -263,23 +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,
|
||||
AllocationMode: resource.AllocationModeImmediate,
|
||||
ParametersRef: parameters,
|
||||
})
|
||||
validClaimTemplate := testClaimTemplate(goodName, goodNS, validClaimSpec)
|
||||
|
||||
scenarios := map[string]struct {
|
||||
oldClaimTemplate *resource.ResourceClaimTemplate
|
||||
@@ -293,36 +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
|
||||
return template
|
||||
},
|
||||
},
|
||||
"invalid-update-mode": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimTemplateSpec {
|
||||
spec := validClaimTemplate.Spec.DeepCopy()
|
||||
spec.Spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer
|
||||
return *spec
|
||||
}(), "field is immutable")},
|
||||
oldClaimTemplate: validClaimTemplate,
|
||||
update: func(template *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate {
|
||||
template.Spec.Spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer
|
||||
template.Spec.Spec.Devices.Requests[0].DeviceClassName += "2"
|
||||
return template
|
||||
},
|
||||
},
|
||||
@@ -331,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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
|
||||
943
pkg/apis/resource/zz_generated.deepcopy.go
generated
943
pkg/apis/resource/zz_generated.deepcopy.go
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user