DRA: new API for 1.31

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

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

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

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

View file

@ -52,13 +52,10 @@ API rule violation: names_match,k8s.io/api/core/v1,VolumeSource,CephFS
API rule violation: names_match,k8s.io/api/core/v1,VolumeSource,StorageOS
API rule violation: names_match,k8s.io/api/networking/v1alpha1,ServiceCIDRSpec,CIDRs
API rule violation: names_match,k8s.io/api/networking/v1beta1,ServiceCIDRSpec,CIDRs
API rule violation: names_match,k8s.io/api/resource/v1alpha3,NamedResourcesAttributeValue,BoolValue
API rule violation: names_match,k8s.io/api/resource/v1alpha3,NamedResourcesAttributeValue,IntSliceValue
API rule violation: names_match,k8s.io/api/resource/v1alpha3,NamedResourcesAttributeValue,IntValue
API rule violation: names_match,k8s.io/api/resource/v1alpha3,NamedResourcesAttributeValue,QuantityValue
API rule violation: names_match,k8s.io/api/resource/v1alpha3,NamedResourcesAttributeValue,StringSliceValue
API rule violation: names_match,k8s.io/api/resource/v1alpha3,NamedResourcesAttributeValue,StringValue
API rule violation: names_match,k8s.io/api/resource/v1alpha3,NamedResourcesAttributeValue,VersionValue
API rule violation: names_match,k8s.io/api/resource/v1alpha3,DeviceAttribute,BoolValue
API rule violation: names_match,k8s.io/api/resource/v1alpha3,DeviceAttribute,IntValue
API rule violation: names_match,k8s.io/api/resource/v1alpha3,DeviceAttribute,StringValue
API rule violation: names_match,k8s.io/api/resource/v1alpha3,DeviceAttribute,VersionValue
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Ref
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Schema
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XEmbeddedResource

View file

@ -1891,6 +1891,26 @@
{
"freshness": "Current",
"resources": [
{
"resource": "deviceclasses",
"responseKind": {
"group": "",
"kind": "DeviceClass",
"version": ""
},
"scope": "Cluster",
"singularResource": "deviceclass",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"resource": "podschedulingcontexts",
"responseKind": {
@ -1926,26 +1946,6 @@
"watch"
]
},
{
"resource": "resourceclaimparameters",
"responseKind": {
"group": "",
"kind": "ResourceClaimParameters",
"version": ""
},
"scope": "Namespaced",
"singularResource": "resourceclaimparameters",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"resource": "resourceclaims",
"responseKind": {
@ -2001,46 +2001,6 @@
"watch"
]
},
{
"resource": "resourceclasses",
"responseKind": {
"group": "",
"kind": "ResourceClass",
"version": ""
},
"scope": "Cluster",
"singularResource": "resourceclass",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"resource": "resourceclassparameters",
"responseKind": {
"group": "",
"kind": "ResourceClassParameters",
"version": ""
},
"scope": "Namespaced",
"singularResource": "resourceclassparameters",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"resource": "resourceslices",
"responseKind": {

View file

@ -1891,6 +1891,26 @@
{
"freshness": "Current",
"resources": [
{
"resource": "deviceclasses",
"responseKind": {
"group": "",
"kind": "DeviceClass",
"version": ""
},
"scope": "Cluster",
"singularResource": "deviceclass",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"resource": "podschedulingcontexts",
"responseKind": {
@ -1926,26 +1946,6 @@
"watch"
]
},
{
"resource": "resourceclaimparameters",
"responseKind": {
"group": "",
"kind": "ResourceClaimParameters",
"version": ""
},
"scope": "Namespaced",
"singularResource": "resourceclaimparameters",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"resource": "resourceclaims",
"responseKind": {
@ -2001,46 +2001,6 @@
"watch"
]
},
{
"resource": "resourceclasses",
"responseKind": {
"group": "",
"kind": "ResourceClass",
"version": ""
},
"scope": "Cluster",
"singularResource": "resourceclass",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"resource": "resourceclassparameters",
"responseKind": {
"group": "",
"kind": "ResourceClassParameters",
"version": ""
},
"scope": "Namespaced",
"singularResource": "resourceclassparameters",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"resource": "resourceslices",
"responseKind": {

View file

@ -3,6 +3,23 @@
"groupVersion": "resource.k8s.io/v1alpha3",
"kind": "APIResourceList",
"resources": [
{
"kind": "DeviceClass",
"name": "deviceclasses",
"namespaced": false,
"singularName": "deviceclass",
"storageVersionHash": "12soGlw2WzE=",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"kind": "PodSchedulingContext",
"name": "podschedulingcontexts",
@ -31,23 +48,6 @@
"update"
]
},
{
"kind": "ResourceClaimParameters",
"name": "resourceclaimparameters",
"namespaced": true,
"singularName": "resourceclaimparameters",
"storageVersionHash": "42WVd9cucrU=",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"kind": "ResourceClaim",
"name": "resourceclaims",
@ -93,40 +93,6 @@
"watch"
]
},
{
"kind": "ResourceClass",
"name": "resourceclasses",
"namespaced": false,
"singularName": "resourceclass",
"storageVersionHash": "gv35DluSP3c=",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"kind": "ResourceClassParameters",
"name": "resourceclassparameters",
"namespaced": true,
"singularName": "resourceclassparameters",
"storageVersionHash": "369PrU9Yi/E=",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"kind": "ResourceSlice",
"name": "resourceslices",

File diff suppressed because it is too large Load diff

View file

@ -6560,6 +6560,10 @@
"default": "",
"description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.",
"type": "string"
},
"request": {
"description": "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.",
"type": "string"
}
},
"required": [
@ -9407,11 +9411,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -1951,11 +1951,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -1463,11 +1463,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -1465,11 +1465,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -1617,11 +1617,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -750,11 +750,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -4300,6 +4300,10 @@
"default": "",
"description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.",
"type": "string"
},
"request": {
"description": "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.",
"type": "string"
}
},
"required": [
@ -6167,11 +6171,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -943,11 +943,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -1652,11 +1652,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -3504,6 +3504,10 @@
"default": "",
"description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.",
"type": "string"
},
"request": {
"description": "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.",
"type": "string"
}
},
"required": [
@ -5371,11 +5375,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -979,11 +979,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -855,11 +855,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -875,11 +875,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -1033,11 +1033,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -993,11 +993,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -1479,11 +1479,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -1480,11 +1480,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -969,11 +969,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -1608,11 +1608,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -1055,11 +1055,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -928,11 +928,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -1047,11 +1047,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -1320,11 +1320,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -846,11 +846,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -2765,11 +2765,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -841,11 +841,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -953,11 +953,6 @@
"group": "",
"kind": "Status",
"version": "v1"
},
{
"group": "resource.k8s.io",
"kind": "Status",
"version": "v1alpha3"
}
]
},

View file

@ -37,7 +37,7 @@ API_KNOWN_VIOLATIONS_DIR="${API_KNOWN_VIOLATIONS_DIR:-"${KUBE_ROOT}/api/api-rule
OUT_DIR="_output"
BOILERPLATE_FILENAME="hack/boilerplate/boilerplate.generatego.txt"
APPLYCONFIG_PKG="k8s.io/client-go/applyconfigurations"
PLURAL_EXCEPTIONS="Endpoints:Endpoints,ResourceClaimParameters:ResourceClaimParameters,ResourceClassParameters:ResourceClassParameters"
PLURAL_EXCEPTIONS="Endpoints:Endpoints"
# Any time we call sort, we want it in the same locale.
export LC_ALL="C"

View file

@ -135,6 +135,10 @@ func TestDefaulting(t *testing.T) {
{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBindingList"}: {},
{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"}: {},
{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBindingList"}: {},
{Group: "resource.k8s.io", Version: "v1alpha3", Kind: "ResourceClaim"}: {},
{Group: "resource.k8s.io", Version: "v1alpha3", Kind: "ResourceClaimList"}: {},
{Group: "resource.k8s.io", Version: "v1alpha3", Kind: "ResourceClaimTemplate"}: {},
{Group: "resource.k8s.io", Version: "v1alpha3", Kind: "ResourceClaimTemplateList"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicy"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyList"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyBinding"}: {},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,112 +0,0 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resource
import "k8s.io/apimachinery/pkg/api/resource"
// NamedResourcesResources is used in ResourceModel.
type NamedResourcesResources struct {
// The list of all individual resources instances currently available.
Instances []NamedResourcesInstance
}
// NamedResourcesInstance represents one individual hardware instance that can be selected based
// on its attributes.
type NamedResourcesInstance struct {
// Name is unique identifier among all resource instances managed by
// the driver on the node. It must be a DNS subdomain.
Name string
// Attributes defines the attributes of this resource instance.
// The name of each attribute must be unique.
Attributes []NamedResourcesAttribute
}
// NamedResourcesAttribute is a combination of an attribute name and its value.
type NamedResourcesAttribute struct {
// Name is unique identifier among all resource instances managed by
// the driver on the node. It must be a DNS subdomain.
Name string
NamedResourcesAttributeValue
}
// NamedResourcesAttributeValue must have one and only one field set.
type NamedResourcesAttributeValue struct {
// QuantityValue is a quantity.
QuantityValue *resource.Quantity
// BoolValue is a true/false value.
BoolValue *bool
// IntValue is a 64-bit integer.
IntValue *int64
// IntSliceValue is an array of 64-bit integers.
IntSliceValue *NamedResourcesIntSlice
// StringValue is a string.
StringValue *string
// StringSliceValue is an array of strings.
StringSliceValue *NamedResourcesStringSlice
// VersionValue is a semantic version according to semver.org spec 2.0.0.
VersionValue *string
}
// NamedResourcesIntSlice contains a slice of 64-bit integers.
type NamedResourcesIntSlice struct {
// Ints is the slice of 64-bit integers.
Ints []int64
}
// NamedResourcesStringSlice contains a slice of strings.
type NamedResourcesStringSlice struct {
// Strings is the slice of strings.
Strings []string
}
// NamedResourcesRequest is used in ResourceRequestModel.
type NamedResourcesRequest struct {
// Selector is a CEL expression which must evaluate to true if a
// resource instance is suitable. The language is as defined in
// https://kubernetes.io/docs/reference/using-api/cel/
//
// In addition, for each type NamedResourcesin AttributeValue there is a map that
// resolves to the corresponding value of the instance under evaluation.
// For example:
//
// attributes.quantity["a"].isGreaterThan(quantity("0")) &&
// attributes.stringslice["b"].isSorted()
Selector string
}
// NamedResourcesFilter is used in ResourceFilterModel.
type NamedResourcesFilter struct {
// Selector is a CEL expression which must evaluate to true if a
// resource instance is suitable. The language is as defined in
// https://kubernetes.io/docs/reference/using-api/cel/
//
// In addition, for each type in NamedResourcesAttributeValue there is a map that
// resolves to the corresponding value of the instance under evaluation.
// For example:
//
// attributes.quantity["a"].isGreaterThan(quantity("0")) &&
// attributes.stringslice["b"].isSorted()
Selector string
}
// NamedResourcesAllocationResult is used in AllocationResultModel.
type NamedResourcesAllocationResult struct {
// Name is the name of the selected resource instance.
Name string
}

View file

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

View file

@ -1,178 +0,0 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validation
import (
"fmt"
"regexp"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
namedresourcescel "k8s.io/dynamic-resource-allocation/structured/namedresources/cel"
corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/apis/resource"
)
var (
validateInstanceName = corevalidation.ValidateDNS1123Subdomain
validateAttributeName = corevalidation.ValidateDNS1123Subdomain
)
type Options struct {
// StoredExpressions must be true if and only if validating CEL
// expressions that were already stored persistently. This makes
// validation more permissive by enabling CEL definitions that are not
// valid yet for new expressions.
StoredExpressions bool
}
func ValidateResources(resources *resource.NamedResourcesResources, fldPath *field.Path) field.ErrorList {
allErrs := validateInstances(resources.Instances, fldPath.Child("instances"))
return allErrs
}
func validateInstances(instances []resource.NamedResourcesInstance, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
instanceNames := sets.New[string]()
for i, instance := range instances {
idxPath := fldPath.Index(i)
instanceName := instance.Name
allErrs = append(allErrs, validateInstanceName(instanceName, idxPath.Child("name"))...)
if instanceNames.Has(instanceName) {
allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), instanceName))
} else {
instanceNames.Insert(instanceName)
}
allErrs = append(allErrs, validateAttributes(instance.Attributes, idxPath.Child("attributes"))...)
}
return allErrs
}
var (
numericIdentifier = `(0|[1-9]\d*)`
preReleaseIdentifier = `(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)`
buildIdentifier = `[0-9a-zA-Z-]+`
semverRe = regexp.MustCompile(`^` +
// dot-separated version segments (e.g. 1.2.3)
numericIdentifier + `\.` + numericIdentifier + `\.` + numericIdentifier +
// optional dot-separated prerelease segments (e.g. -alpha.PRERELEASE.1)
`(-` + preReleaseIdentifier + `(\.` + preReleaseIdentifier + `)*)?` +
// optional dot-separated build identifier segments (e.g. +build.id.20240305)
`(\+` + buildIdentifier + `(\.` + buildIdentifier + `)*)?` +
`$`)
)
func validateAttributes(attributes []resource.NamedResourcesAttribute, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
attributeNames := sets.New[string]()
for i, attribute := range attributes {
idxPath := fldPath.Index(i)
attributeName := attribute.Name
allErrs = append(allErrs, validateAttributeName(attributeName, idxPath.Child("name"))...)
if attributeNames.Has(attributeName) {
allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), attributeName))
} else {
attributeNames.Insert(attributeName)
}
entries := sets.New[string]()
if attribute.QuantityValue != nil {
entries.Insert("quantity")
}
if attribute.BoolValue != nil {
entries.Insert("bool")
}
if attribute.IntValue != nil {
entries.Insert("int")
}
if attribute.IntSliceValue != nil {
entries.Insert("intSlice")
}
if attribute.StringValue != nil {
entries.Insert("string")
}
if attribute.StringSliceValue != nil {
entries.Insert("stringSlice")
}
if attribute.VersionValue != nil {
entries.Insert("version")
if !semverRe.MatchString(*attribute.VersionValue) {
allErrs = append(allErrs, field.Invalid(idxPath.Child("version"), *attribute.VersionValue, "must be a string compatible with semver.org spec 2.0.0"))
}
}
switch len(entries) {
case 0:
allErrs = append(allErrs, field.Required(idxPath, "exactly one value must be set"))
case 1:
// Okay.
default:
allErrs = append(allErrs, field.Invalid(idxPath, sets.List(entries), "exactly one field must be set, not several"))
}
}
return allErrs
}
func ValidateRequest(opts Options, request *resource.NamedResourcesRequest, fldPath *field.Path) field.ErrorList {
return validateSelector(opts, request.Selector, fldPath.Child("selector"))
}
func ValidateFilter(opts Options, filter *resource.NamedResourcesFilter, fldPath *field.Path) field.ErrorList {
return validateSelector(opts, filter.Selector, fldPath.Child("selector"))
}
func validateSelector(opts Options, selector string, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if selector == "" {
allErrs = append(allErrs, field.Required(fldPath, ""))
} else {
envType := environment.NewExpressions
if opts.StoredExpressions {
envType = environment.StoredExpressions
}
result := namedresourcescel.GetCompiler().CompileCELExpression(selector, envType)
if result.Error != nil {
allErrs = append(allErrs, convertCELErrorToValidationError(fldPath, selector, result.Error))
}
}
return allErrs
}
func convertCELErrorToValidationError(fldPath *field.Path, expression string, err *cel.Error) *field.Error {
switch err.Type {
case cel.ErrorTypeRequired:
return field.Required(fldPath, err.Detail)
case cel.ErrorTypeInvalid:
return field.Invalid(fldPath, expression, err.Detail)
case cel.ErrorTypeInternal:
return field.InternalError(fldPath, err)
}
return field.InternalError(fldPath, fmt.Errorf("unsupported error type: %w", err))
}
func ValidateAllocationResult(result *resource.NamedResourcesAllocationResult, fldPath *field.Path) field.ErrorList {
return validateInstanceName(result.Name, fldPath.Child("name"))
}

View file

@ -1,188 +0,0 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validation
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/util/validation/field"
resourceapi "k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/utils/ptr"
)
func testResources(instances []resourceapi.NamedResourcesInstance) *resourceapi.NamedResourcesResources {
resources := &resourceapi.NamedResourcesResources{
Instances: instances,
}
return resources
}
func TestValidateResources(t *testing.T) {
goodName := "foo"
badName := "!@#$%^"
quantity := resource.MustParse("1")
scenarios := map[string]struct {
resources *resourceapi.NamedResourcesResources
wantFailures field.ErrorList
}{
"empty": {
resources: testResources(nil),
},
"good": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName}}),
},
"bad-name": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: badName}}),
},
"duplicate-name": {
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("instances").Index(1).Child("name"), goodName)},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName}, {Name: goodName}}),
},
"quantity": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{QuantityValue: &quantity}}}}}),
},
"bool": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{BoolValue: ptr.To(true)}}}}}),
},
"int": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{IntValue: ptr.To(int64(1))}}}}}),
},
"int-slice": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{IntSliceValue: &resourceapi.NamedResourcesIntSlice{Ints: []int64{1, 2, 3}}}}}}}),
},
"string": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{StringValue: ptr.To("hello")}}}}}),
},
"string-slice": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{StringSliceValue: &resourceapi.NamedResourcesStringSlice{Strings: []string{"hello"}}}}}}}),
},
"version-okay": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0")}}}}}),
},
"version-beta": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta")}}}}}),
},
"version-beta-1": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta.1")}}}}}),
},
"version-build": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0+build")}}}}}),
},
"version-build-1": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0+build.1")}}}}}),
},
"version-beta-1-build-1": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta.1+build.1")}}}}}),
},
"version-bad": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.0", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0")}}}}}),
},
"version-bad-leading-zeros": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "01.0.0", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("01.0.0")}}}}}),
},
"version-bad-leading-zeros-middle": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.00.0", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.00.0")}}}}}),
},
"version-bad-leading-zeros-end": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.0.00", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.00")}}}}}),
},
"version-bad-spaces": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), " 1.0.0 ", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To(" 1.0.0 ")}}}}}),
},
"empty-attribute": {
wantFailures: field.ErrorList{field.Required(field.NewPath("instances").Index(0).Child("attributes").Index(0), "exactly one value must be set")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName}}}}),
},
"duplicate-value": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0), []string{"bool", "int"}, "exactly one field must be set, not several")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{BoolValue: ptr.To(true), IntValue: ptr.To(int64(1))}}}}}),
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
errs := ValidateResources(scenario.resources, nil)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}
func TestValidateSelector(t *testing.T) {
scenarios := map[string]struct {
selector string
wantFailures field.ErrorList
}{
"okay": {
selector: "true",
},
"empty": {
selector: "",
wantFailures: field.ErrorList{field.Required(nil, "")},
},
"undefined": {
selector: "nosuchvar",
wantFailures: field.ErrorList{field.Invalid(nil, "nosuchvar", "compilation failed: ERROR: <input>:1:1: undeclared reference to 'nosuchvar' (in container '')\n | nosuchvar\n | ^")},
},
"wrong-type": {
selector: "1",
wantFailures: field.ErrorList{field.Invalid(nil, "1", "must evaluate to bool")},
},
"quantity": {
selector: `attributes.quantity["name"].isGreaterThan(quantity("0"))`,
},
"bool": {
selector: `attributes.bool["name"]`,
},
"int": {
selector: `attributes.int["name"] > 0`,
},
"intslice": {
selector: `attributes.intslice["name"].isSorted()`,
},
"string": {
selector: `attributes.string["name"] == "fish"`,
},
"stringslice": {
selector: `attributes.stringslice["name"].isSorted()`,
},
"version": {
selector: `attributes.version["name"].isGreaterThan(semver("1.0.0"))`,
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
// At the moment, there's no difference between stored and new expressions.
// This uses the stricter validation.
opts := Options{
StoredExpressions: false,
}
errs := validateSelector(opts, scenario.selector, nil)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -0,0 +1,86 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha3_test
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
v1alpha3 "k8s.io/api/resource/v1alpha3"
"k8s.io/apimachinery/pkg/runtime"
// ensure types are installed
"k8s.io/kubernetes/pkg/api/legacyscheme"
_ "k8s.io/kubernetes/pkg/apis/resource/install"
)
func TestSetDefaultAllocationMode(t *testing.T) {
claim := &v1alpha3.ResourceClaim{
Spec: v1alpha3.ResourceClaimSpec{
Devices: v1alpha3.DeviceClaim{
Requests: []v1alpha3.DeviceRequest{{}},
},
},
}
// fields should be defaulted
defaultMode := v1alpha3.DeviceAllocationModeExactCount
defaultCount := int64(1)
output := roundTrip(t, runtime.Object(claim)).(*v1alpha3.ResourceClaim)
assert.Equal(t, defaultMode, output.Spec.Devices.Requests[0].AllocationMode)
assert.Equal(t, defaultCount, output.Spec.Devices.Requests[0].Count)
// field should not change
nonDefaultMode := v1alpha3.DeviceAllocationModeExactCount
nonDefaultCount := int64(10)
claim = &v1alpha3.ResourceClaim{
Spec: v1alpha3.ResourceClaimSpec{
Devices: v1alpha3.DeviceClaim{
Requests: []v1alpha3.DeviceRequest{{
AllocationMode: nonDefaultMode,
Count: nonDefaultCount,
}},
},
},
}
output = roundTrip(t, runtime.Object(claim)).(*v1alpha3.ResourceClaim)
assert.Equal(t, nonDefaultMode, output.Spec.Devices.Requests[0].AllocationMode)
assert.Equal(t, nonDefaultCount, output.Spec.Devices.Requests[0].Count)
}
func roundTrip(t *testing.T, obj runtime.Object) runtime.Object {
codec := legacyscheme.Codecs.LegacyCodec(v1alpha3.SchemeGroupVersion)
data, err := runtime.Encode(codec, obj)
if err != nil {
t.Errorf("%v\n %#v", err, obj)
return nil
}
obj2, err := runtime.Decode(codec, data)
if err != nil {
t.Errorf("%v\nData: %s\nSource: %#v", err, string(data), obj)
return nil
}
obj3 := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(runtime.Object)
err = legacyscheme.Scheme.Convert(obj2, obj3, nil)
if err != nil {
t.Errorf("%v\nSource: %#v", err, obj2)
return nil
}
return obj3
}

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,247 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validation
import (
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/utils/ptr"
)
func testClass(name string) *resource.DeviceClass {
return &resource.DeviceClass{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}
}
func TestValidateClass(t *testing.T) {
goodName := "foo"
now := metav1.Now()
badName := "!@#$%^"
badValue := "spaces not allowed"
scenarios := map[string]struct {
class *resource.DeviceClass
wantFailures field.ErrorList
}{
"good-class": {
class: testClass(goodName),
},
"missing-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
class: testClass(""),
},
"bad-name": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
class: testClass(badName),
},
"generate-name": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.GenerateName = "pvc-"
return class
}(),
},
"uid": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
return class
}(),
},
"resource-version": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.ResourceVersion = "1"
return class
}(),
},
"generation": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Generation = 100
return class
}(),
},
"creation-timestamp": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.CreationTimestamp = now
return class
}(),
},
"deletion-grace-period-seconds": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.DeletionGracePeriodSeconds = ptr.To(int64(10))
return class
}(),
},
"owner-references": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "pod",
Name: "foo",
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
},
}
return class
}(),
},
"finalizers": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Finalizers = []string{
"example.com/foo",
}
return class
}(),
},
"managed-fields": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.ManagedFields = []metav1.ManagedFieldsEntry{
{
FieldsType: "FieldsV1",
Operation: "Apply",
APIVersion: "apps/v1",
Manager: "foo",
},
}
return class
}(),
},
"good-labels": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Labels = map[string]string{
"apps.kubernetes.io/name": "test",
}
return class
}(),
},
"bad-labels": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Labels = map[string]string{
"hello-world": badValue,
}
return class
}(),
},
"good-annotations": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Annotations = map[string]string{
"foo": "bar",
}
return class
}(),
},
"bad-annotations": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Annotations = map[string]string{
badName: "hello world",
}
return class
}(),
},
"invalid-node-selector": {
wantFailures: field.ErrorList{field.Required(field.NewPath("suitableNodes", "nodeSelectorTerms"), "must have at least one node selector term")},
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Spec.SuitableNodes = &core.NodeSelector{
// Must not be empty.
}
return class
}(),
},
"valid-node-selector": {
class: func() *resource.DeviceClass {
class := testClass(goodName)
class.Spec.SuitableNodes = &core.NodeSelector{
NodeSelectorTerms: []core.NodeSelectorTerm{{
MatchExpressions: []core.NodeSelectorRequirement{{
Key: "foo",
Operator: core.NodeSelectorOpDoesNotExist,
}},
}},
}
return class
}(),
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
errs := ValidateDeviceClass(scenario.class)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}
func TestValidateClassUpdate(t *testing.T) {
validClass := testClass(goodName)
scenarios := map[string]struct {
oldClass *resource.DeviceClass
update func(class *resource.DeviceClass) *resource.DeviceClass
wantFailures field.ErrorList
}{
"valid-no-op-update": {
oldClass: validClass,
update: func(class *resource.DeviceClass) *resource.DeviceClass { return class },
},
"update-node-selector": {
oldClass: validClass,
update: func(class *resource.DeviceClass) *resource.DeviceClass {
class = class.DeepCopy()
class.Spec.SuitableNodes = &core.NodeSelector{
NodeSelectorTerms: []core.NodeSelectorTerm{{
MatchExpressions: []core.NodeSelectorRequirement{{
Key: "foo",
Operator: core.NodeSelectorOpDoesNotExist,
}},
}},
}
return class
},
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
scenario.oldClass.ResourceVersion = "1"
errs := ValidateDeviceClassUpdate(scenario.update(scenario.oldClass.DeepCopy()), scenario.oldClass)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}

View file

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

View file

@ -1,306 +0,0 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validation
import (
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/utils/ptr"
)
func testResourceClaimParameters(name, namespace string, requests []resource.DriverRequests) *resource.ResourceClaimParameters {
return &resource.ResourceClaimParameters{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
DriverRequests: requests,
}
}
var goodRequests []resource.DriverRequests
func TestValidateResourceClaimParameters(t *testing.T) {
goodName := "foo"
badName := "!@#$%^"
badValue := "spaces not allowed"
now := metav1.Now()
scenarios := map[string]struct {
parameters *resource.ResourceClaimParameters
wantFailures field.ErrorList
}{
"good": {
parameters: testResourceClaimParameters(goodName, goodName, goodRequests),
},
"missing-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
parameters: testResourceClaimParameters("", goodName, goodRequests),
},
"missing-namespace": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
parameters: testResourceClaimParameters(goodName, "", goodRequests),
},
"bad-name": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
parameters: testResourceClaimParameters(badName, goodName, goodRequests),
},
"bad-namespace": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')")},
parameters: testResourceClaimParameters(goodName, badName, goodRequests),
},
"generate-name": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.GenerateName = "prefix-"
return parameters
}(),
},
"uid": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
return parameters
}(),
},
"resource-version": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.ResourceVersion = "1"
return parameters
}(),
},
"generation": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.Generation = 100
return parameters
}(),
},
"creation-timestamp": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.CreationTimestamp = now
return parameters
}(),
},
"deletion-grace-period-seconds": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.DeletionGracePeriodSeconds = ptr.To[int64](10)
return parameters
}(),
},
"owner-references": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "pod",
Name: "foo",
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
},
}
return parameters
}(),
},
"finalizers": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.Finalizers = []string{
"example.com/foo",
}
return parameters
}(),
},
"managed-fields": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.ManagedFields = []metav1.ManagedFieldsEntry{
{
FieldsType: "FieldsV1",
Operation: "Apply",
APIVersion: "apps/v1",
Manager: "foo",
},
}
return parameters
}(),
},
"good-labels": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.Labels = map[string]string{
"apps.kubernetes.io/name": "test",
}
return parameters
}(),
},
"bad-labels": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.Labels = map[string]string{
"hello-world": badValue,
}
return parameters
}(),
},
"good-annotations": {
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.Annotations = map[string]string{
"foo": "bar",
}
return parameters
}(),
},
"bad-annotations": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.Annotations = map[string]string{
badName: "hello world",
}
return parameters
}(),
},
"empty-model": {
wantFailures: field.ErrorList{field.Required(field.NewPath("driverRequests").Index(0).Child("requests").Index(0), "exactly one structured model field must be set")},
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.DriverRequests = []resource.DriverRequests{{DriverName: goodName, Requests: []resource.ResourceRequest{{}}}}
return parameters
}(),
},
"empty-requests": {
wantFailures: field.ErrorList{field.Required(field.NewPath("driverRequests").Index(0).Child("requests"), "empty entries with no requests are not allowed")},
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.DriverRequests = []resource.DriverRequests{{DriverName: goodName}}
return parameters
}(),
},
"invalid-driver": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverRequests").Index(1).Child("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.DriverRequests = []resource.DriverRequests{
{
DriverName: goodName,
Requests: []resource.ResourceRequest{
{
ResourceRequestModel: resource.ResourceRequestModel{
NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
},
},
},
},
{
DriverName: badName,
Requests: []resource.ResourceRequest{
{
ResourceRequestModel: resource.ResourceRequestModel{
NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
},
},
},
},
}
return parameters
}(),
},
"duplicate-driver": {
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("driverRequests").Index(1).Child("driverName"), goodName)},
parameters: func() *resource.ResourceClaimParameters {
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
parameters.DriverRequests = []resource.DriverRequests{
{
DriverName: goodName,
Requests: []resource.ResourceRequest{
{
ResourceRequestModel: resource.ResourceRequestModel{
NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
},
},
},
},
{
DriverName: goodName,
Requests: []resource.ResourceRequest{
{
ResourceRequestModel: resource.ResourceRequestModel{
NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
},
},
},
},
}
return parameters
}(),
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
errs := ValidateResourceClaimParameters(scenario.parameters)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}
func TestValidateResourceClaimParametersUpdate(t *testing.T) {
name := "valid"
validResourceClaimParameters := testResourceClaimParameters(name, name, nil)
scenarios := map[string]struct {
oldResourceClaimParameters *resource.ResourceClaimParameters
update func(claim *resource.ResourceClaimParameters) *resource.ResourceClaimParameters
wantFailures field.ErrorList
}{
"valid-no-op-update": {
oldResourceClaimParameters: validResourceClaimParameters,
update: func(claim *resource.ResourceClaimParameters) *resource.ResourceClaimParameters { return claim },
},
"invalid-name-update": {
oldResourceClaimParameters: validResourceClaimParameters,
update: func(claim *resource.ResourceClaimParameters) *resource.ResourceClaimParameters {
claim.Name += "-update"
return claim
},
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
scenario.oldResourceClaimParameters.ResourceVersion = "1"
errs := ValidateResourceClaimParametersUpdate(scenario.update(scenario.oldResourceClaimParameters.DeepCopy()), scenario.oldResourceClaimParameters)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}

View file

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

View file

@ -1,301 +0,0 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validation
import (
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/utils/pointer"
)
func testClass(name, driverName string) *resource.ResourceClass {
return &resource.ResourceClass{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
DriverName: driverName,
}
}
func TestValidateClass(t *testing.T) {
goodName := "foo"
now := metav1.Now()
goodParameters := resource.ResourceClassParametersReference{
Name: "valid",
Namespace: "valid",
Kind: "foo",
}
badName := "!@#$%^"
badValue := "spaces not allowed"
badAPIGroup := "example.com/v1"
goodAPIGroup := "example.com"
scenarios := map[string]struct {
class *resource.ResourceClass
wantFailures field.ErrorList
}{
"good-class": {
class: testClass(goodName, goodName),
},
"good-long-driver-name": {
class: testClass(goodName, "acme.example.com"),
},
"missing-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
class: testClass("", goodName),
},
"bad-name": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
class: testClass(badName, goodName),
},
"generate-name": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.GenerateName = "pvc-"
return class
}(),
},
"uid": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
return class
}(),
},
"resource-version": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ResourceVersion = "1"
return class
}(),
},
"generation": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.Generation = 100
return class
}(),
},
"creation-timestamp": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.CreationTimestamp = now
return class
}(),
},
"deletion-grace-period-seconds": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.DeletionGracePeriodSeconds = pointer.Int64(10)
return class
}(),
},
"owner-references": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "pod",
Name: "foo",
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
},
}
return class
}(),
},
"finalizers": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.Finalizers = []string{
"example.com/foo",
}
return class
}(),
},
"managed-fields": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ManagedFields = []metav1.ManagedFieldsEntry{
{
FieldsType: "FieldsV1",
Operation: "Apply",
APIVersion: "apps/v1",
Manager: "foo",
},
}
return class
}(),
},
"good-labels": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.Labels = map[string]string{
"apps.kubernetes.io/name": "test",
}
return class
}(),
},
"bad-labels": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.Labels = map[string]string{
"hello-world": badValue,
}
return class
}(),
},
"good-annotations": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.Annotations = map[string]string{
"foo": "bar",
}
return class
}(),
},
"bad-annotations": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.Annotations = map[string]string{
badName: "hello world",
}
return class
}(),
},
"missing-driver-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("driverName"), ""),
field.Invalid(field.NewPath("driverName"), "", "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
},
class: testClass(goodName, ""),
},
"invalid-driver-name": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
class: testClass(goodName, badName),
},
"invalid-qualified-driver-name": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), goodName+"/path", "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
class: testClass(goodName, goodName+"/path"),
},
"good-parameters": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ParametersRef = goodParameters.DeepCopy()
return class
}(),
},
"good-parameters-apigroup": {
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ParametersRef = goodParameters.DeepCopy()
class.ParametersRef.APIGroup = goodAPIGroup
return class
}(),
},
"bad-parameters-apigroup": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("parametersRef", "apiGroup"), badAPIGroup, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ParametersRef = goodParameters.DeepCopy()
class.ParametersRef.APIGroup = badAPIGroup
return class
}(),
},
"missing-parameters-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("parametersRef", "name"), "")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ParametersRef = goodParameters.DeepCopy()
class.ParametersRef.Name = ""
return class
}(),
},
"bad-parameters-namespace": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("parametersRef", "namespace"), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ParametersRef = goodParameters.DeepCopy()
class.ParametersRef.Namespace = badName
return class
}(),
},
"missing-parameters-kind": {
wantFailures: field.ErrorList{field.Required(field.NewPath("parametersRef", "kind"), "")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.ParametersRef = goodParameters.DeepCopy()
class.ParametersRef.Kind = ""
return class
}(),
},
"invalid-node-selector": {
wantFailures: field.ErrorList{field.Required(field.NewPath("suitableNodes", "nodeSelectorTerms"), "must have at least one node selector term")},
class: func() *resource.ResourceClass {
class := testClass(goodName, goodName)
class.SuitableNodes = &core.NodeSelector{
// Must not be empty.
}
return class
}(),
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
errs := ValidateClass(scenario.class)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}
func TestValidateClassUpdate(t *testing.T) {
validClass := testClass("foo", "valid")
scenarios := map[string]struct {
oldClass *resource.ResourceClass
update func(class *resource.ResourceClass) *resource.ResourceClass
wantFailures field.ErrorList
}{
"valid-no-op-update": {
oldClass: validClass,
update: func(class *resource.ResourceClass) *resource.ResourceClass { return class },
},
"update-driver": {
oldClass: validClass,
update: func(class *resource.ResourceClass) *resource.ResourceClass {
class.DriverName += "2"
return class
},
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
scenario.oldClass.ResourceVersion = "1"
errs := ValidateClassUpdate(scenario.update(scenario.oldClass.DeepCopy()), scenario.oldClass)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}

View file

@ -1,313 +0,0 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validation
import (
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/utils/ptr"
)
func testResourceClassParameters(name, namespace string, filters []resource.ResourceFilter) *resource.ResourceClassParameters {
return &resource.ResourceClassParameters{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Filters: filters,
}
}
var goodFilters []resource.ResourceFilter
func TestValidateResourceClassParameters(t *testing.T) {
goodName := "foo"
badName := "!@#$%^"
badValue := "spaces not allowed"
now := metav1.Now()
scenarios := map[string]struct {
parameters *resource.ResourceClassParameters
wantFailures field.ErrorList
}{
"good": {
parameters: testResourceClassParameters(goodName, goodName, goodFilters),
},
"missing-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
parameters: testResourceClassParameters("", goodName, goodFilters),
},
"missing-namespace": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
parameters: testResourceClassParameters(goodName, "", goodFilters),
},
"bad-name": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
parameters: testResourceClassParameters(badName, goodName, goodFilters),
},
"bad-namespace": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')")},
parameters: testResourceClassParameters(goodName, badName, goodFilters),
},
"generate-name": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.GenerateName = "prefix-"
return parameters
}(),
},
"uid": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
return parameters
}(),
},
"resource-version": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.ResourceVersion = "1"
return parameters
}(),
},
"generation": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Generation = 100
return parameters
}(),
},
"creation-timestamp": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.CreationTimestamp = now
return parameters
}(),
},
"deletion-grace-period-seconds": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.DeletionGracePeriodSeconds = ptr.To[int64](10)
return parameters
}(),
},
"owner-references": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "pod",
Name: "foo",
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
},
}
return parameters
}(),
},
"finalizers": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Finalizers = []string{
"example.com/foo",
}
return parameters
}(),
},
"managed-fields": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.ManagedFields = []metav1.ManagedFieldsEntry{
{
FieldsType: "FieldsV1",
Operation: "Apply",
APIVersion: "apps/v1",
Manager: "foo",
},
}
return parameters
}(),
},
"good-labels": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Labels = map[string]string{
"apps.kubernetes.io/name": "test",
}
return parameters
}(),
},
"bad-labels": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Labels = map[string]string{
"hello-world": badValue,
}
return parameters
}(),
},
"good-annotations": {
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Annotations = map[string]string{
"foo": "bar",
}
return parameters
}(),
},
"bad-annotations": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Annotations = map[string]string{
badName: "hello world",
}
return parameters
}(),
},
"empty-model": {
wantFailures: field.ErrorList{field.Required(field.NewPath("filters").Index(0), "exactly one structured model field must be set")},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Filters = []resource.ResourceFilter{{DriverName: goodName}}
return parameters
}(),
},
"filters-invalid-driver": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("filters").Index(1).Child("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Filters = []resource.ResourceFilter{
{
DriverName: goodName,
ResourceFilterModel: resource.ResourceFilterModel{
NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
},
},
{
DriverName: badName,
ResourceFilterModel: resource.ResourceFilterModel{
NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
},
},
}
return parameters
}(),
},
"filters-duplicate-driver": {
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("filters").Index(1).Child("driverName"), goodName)},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.Filters = []resource.ResourceFilter{
{
DriverName: goodName,
ResourceFilterModel: resource.ResourceFilterModel{
NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
},
},
{
DriverName: goodName,
ResourceFilterModel: resource.ResourceFilterModel{
NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
},
},
}
return parameters
}(),
},
"parameters-invalid-driver": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("vendorParameters").Index(1).Child("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.VendorParameters = []resource.VendorParameters{
{
DriverName: goodName,
},
{
DriverName: badName,
},
}
return parameters
}(),
},
"parameters-duplicate-driver": {
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("vendorParameters").Index(1).Child("driverName"), goodName)},
parameters: func() *resource.ResourceClassParameters {
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
parameters.VendorParameters = []resource.VendorParameters{
{
DriverName: goodName,
},
{
DriverName: goodName,
},
}
return parameters
}(),
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
errs := ValidateResourceClassParameters(scenario.parameters)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}
func TestValidateResourceClassParametersUpdate(t *testing.T) {
name := "valid"
validResourceClassParameters := testResourceClassParameters(name, name, nil)
scenarios := map[string]struct {
oldResourceClassParameters *resource.ResourceClassParameters
update func(class *resource.ResourceClassParameters) *resource.ResourceClassParameters
wantFailures field.ErrorList
}{
"valid-no-op-update": {
oldResourceClassParameters: validResourceClassParameters,
update: func(class *resource.ResourceClassParameters) *resource.ResourceClassParameters { return class },
},
"invalid-name-update": {
oldResourceClassParameters: validResourceClassParameters,
update: func(class *resource.ResourceClassParameters) *resource.ResourceClassParameters {
class.Name += "-update"
return class
},
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
scenario.oldResourceClassParameters.ResourceVersion = "1"
errs := ValidateResourceClassParametersUpdate(scenario.update(scenario.oldResourceClassParameters.DeepCopy()), scenario.oldResourceClassParameters)
assert.Equal(t, scenario.wantFailures, errs)
})
}
}

View file

@ -32,10 +32,13 @@ func testResourceSlice(name, nodeName, driverName string) *resource.ResourceSlic
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
NodeName: nodeName,
DriverName: driverName,
ResourceModel: resource.ResourceModel{
NamedResources: &resource.NamedResourcesResources{},
Spec: resource.ResourceSliceSpec{
NodeName: nodeName,
Driver: driverName,
Pool: resource.ResourcePool{
Name: nodeName,
ResourceSliceCount: 1,
},
},
}
}
@ -180,22 +183,24 @@ func TestValidateResourceSlice(t *testing.T) {
}(),
},
"bad-nodename": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("nodeName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
slice: testResourceSlice(goodName, badName, driverName),
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "pool", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
field.Invalid(field.NewPath("spec", "nodeName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
},
slice: testResourceSlice(goodName, badName, driverName),
},
"bad-multi-pool-name": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "pool", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
field.Invalid(field.NewPath("spec", "pool", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
field.Invalid(field.NewPath("spec", "nodeName"), badName+"/"+badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
},
slice: testResourceSlice(goodName, badName+"/"+badName, driverName),
},
"bad-drivername": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "driver"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
slice: testResourceSlice(goodName, goodName, badName),
},
"empty-model": {
wantFailures: field.ErrorList{field.Required(nil, "exactly one structured model field must be set")},
slice: func() *resource.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName)
slice.ResourceModel = resource.ResourceModel{}
return slice
}(),
},
}
for name, scenario := range scenarios {
@ -228,18 +233,26 @@ func TestValidateResourceSliceUpdate(t *testing.T) {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
},
"invalid-update-nodename": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("nodeName"), name+"-updated", "field is immutable")},
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "nodeName"), name+"-updated", "field is immutable")},
oldResourceSlice: validResourceSlice,
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
slice.NodeName += "-updated"
slice.Spec.NodeName += "-updated"
return slice
},
},
"invalid-update-drivername": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), name+"-updated", "field is immutable")},
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "driver"), name+"-updated", "field is immutable")},
oldResourceSlice: validResourceSlice,
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
slice.DriverName += "-updated"
slice.Spec.Driver += "-updated"
return slice
},
},
"invalid-update-pool": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "pool", "name"), validResourceSlice.Spec.Pool.Name+"-updated", "field is immutable")},
oldResourceSlice: validResourceSlice,
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
slice.Spec.Pool.Name += "-updated"
return slice
},
},

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,6 @@ import (
"fmt"
"sync"
resourceapi "k8s.io/api/resource/v1alpha3"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/kubernetes/pkg/kubelet/checkpointmanager"
@ -56,9 +55,6 @@ type ClaimInfoState struct {
// PodUIDs is a set of pod UIDs that reference a resource
PodUIDs sets.Set[string]
// ResourceHandles is a list of opaque resource data for processing by a specific kubelet plugin
ResourceHandles []resourceapi.ResourceHandle
// CDIDevices is a map of DriverName --> CDI devices returned by the
// GRPC API call NodePrepareResource
CDIDevices map[string][]string

View file

@ -22,7 +22,6 @@ limitations under the License.
package state
import (
v1alpha3 "k8s.io/api/resource/v1alpha3"
sets "k8s.io/apimachinery/pkg/util/sets"
)
@ -36,13 +35,6 @@ func (in *ClaimInfoState) DeepCopyInto(out *ClaimInfoState) {
(*out)[key] = val
}
}
if in.ResourceHandles != nil {
in, out := &in.ResourceHandles, &out.ResourceHandles
*out = make([]v1alpha3.ResourceHandle, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.CDIDevices != nil {
in, out := &in.CDIDevices, &out.CDIDevices
*out = make(map[string][]string, len(*in))

View file

@ -623,17 +623,15 @@ func AddHandlers(h printers.PrintHandler) {
}
_ = h.TableHandler(scaleColumnDefinitions, printScale)
resourceClassColumnDefinitions := []metav1.TableColumnDefinition{
deviceClassColumnDefinitions := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "DriverName", Type: "string", Description: resourceapi.ResourceClass{}.SwaggerDoc()["driverName"]},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
}
_ = h.TableHandler(resourceClassColumnDefinitions, printResourceClass)
_ = h.TableHandler(resourceClassColumnDefinitions, printResourceClassList)
_ = h.TableHandler(deviceClassColumnDefinitions, printDeviceClass)
_ = h.TableHandler(deviceClassColumnDefinitions, printDeviceClassList)
resourceClaimColumnDefinitions := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "ResourceClassName", Type: "string", Description: resourceapi.ResourceClaimSpec{}.SwaggerDoc()["resourceClassName"]},
{Name: "State", Type: "string", Description: "A summary of the current state (allocated, pending, reserved, etc.)."},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
}
@ -642,7 +640,6 @@ func AddHandlers(h printers.PrintHandler) {
resourceClaimTemplateColumnDefinitions := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "ResourceClassName", Type: "string", Description: resourceapi.ResourceClaimSpec{}.SwaggerDoc()["resourceClassName"]},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
}
_ = h.TableHandler(resourceClaimTemplateColumnDefinitions, printResourceClaimTemplate)
@ -656,30 +653,15 @@ func AddHandlers(h printers.PrintHandler) {
_ = h.TableHandler(podSchedulingCtxColumnDefinitions, printPodSchedulingContext)
_ = h.TableHandler(podSchedulingCtxColumnDefinitions, printPodSchedulingContextList)
resourceClaimParametersColumnDefinitions := []metav1.TableColumnDefinition{
nodeResourceSliceColumnDefinitions := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "GeneratedFrom", Type: "string", Description: resourceapi.ResourceClaimParameters{}.SwaggerDoc()["generatedFrom"]},
{Name: "Node", Type: "string", Description: resourceapi.ResourceSliceSpec{}.SwaggerDoc()["nodeName"]},
{Name: "Driver", Type: "string", Description: resourceapi.ResourceSliceSpec{}.SwaggerDoc()["driver"]},
{Name: "Pool", Type: "string", Description: resourceapi.ResourcePool{}.SwaggerDoc()["name"]},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
}
_ = h.TableHandler(resourceClaimParametersColumnDefinitions, printResourceClaimParameters)
_ = h.TableHandler(resourceClaimParametersColumnDefinitions, printResourceClaimParametersList)
resourceClassParametersColumnDefinitions := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "GeneratedFrom", Type: "string", Description: resourceapi.ResourceClassParameters{}.SwaggerDoc()["generatedFrom"]},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
}
_ = h.TableHandler(resourceClassParametersColumnDefinitions, printResourceClassParameters)
_ = h.TableHandler(resourceClassParametersColumnDefinitions, printResourceClassParametersList)
nodeResourceCapacityColumnDefinitions := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "Node", Type: "string", Description: resourceapi.ResourceSlice{}.SwaggerDoc()["nodeName"]},
{Name: "Driver", Type: "string", Description: resourceapi.ResourceSlice{}.SwaggerDoc()["driverName"]},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
}
_ = h.TableHandler(nodeResourceCapacityColumnDefinitions, printResourceSlice)
_ = h.TableHandler(nodeResourceCapacityColumnDefinitions, printResourceSliceList)
_ = h.TableHandler(nodeResourceSliceColumnDefinitions, printResourceSlice)
_ = h.TableHandler(nodeResourceSliceColumnDefinitions, printResourceSliceList)
serviceCIDRColumnDefinitions := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
@ -2979,19 +2961,19 @@ func printScale(obj *autoscaling.Scale, options printers.GenerateOptions) ([]met
return []metav1.TableRow{row}, nil
}
func printResourceClass(obj *resource.ResourceClass, options printers.GenerateOptions) ([]metav1.TableRow, error) {
func printDeviceClass(obj *resource.DeviceClass, options printers.GenerateOptions) ([]metav1.TableRow, error) {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},
}
row.Cells = append(row.Cells, obj.Name, obj.DriverName, translateTimestampSince(obj.CreationTimestamp))
row.Cells = append(row.Cells, obj.Name, translateTimestampSince(obj.CreationTimestamp))
return []metav1.TableRow{row}, nil
}
func printResourceClassList(list *resource.ResourceClassList, options printers.GenerateOptions) ([]metav1.TableRow, error) {
func printDeviceClassList(list *resource.DeviceClassList, options printers.GenerateOptions) ([]metav1.TableRow, error) {
rows := make([]metav1.TableRow, 0, len(list.Items))
for i := range list.Items {
r, err := printResourceClass(&list.Items[i], options)
r, err := printDeviceClass(&list.Items[i], options)
if err != nil {
return nil, err
}
@ -3004,7 +2986,7 @@ func printResourceClaim(obj *resource.ResourceClaim, options printers.GenerateOp
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},
}
row.Cells = append(row.Cells, obj.Name, obj.Spec.ResourceClassName, resourceClaimState(obj), translateTimestampSince(obj.CreationTimestamp))
row.Cells = append(row.Cells, obj.Name, resourceClaimState(obj), translateTimestampSince(obj.CreationTimestamp))
return []metav1.TableRow{row}, nil
}
@ -3045,7 +3027,7 @@ func printResourceClaimTemplate(obj *resource.ResourceClaimTemplate, options pri
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},
}
row.Cells = append(row.Cells, obj.Name, obj.Spec.Spec.ResourceClassName, translateTimestampSince(obj.CreationTimestamp))
row.Cells = append(row.Cells, obj.Name, translateTimestampSince(obj.CreationTimestamp))
return []metav1.TableRow{row}, nil
}
@ -3083,61 +3065,11 @@ func printPodSchedulingContextList(list *resource.PodSchedulingContextList, opti
return rows, nil
}
func printResourceClaimParameters(obj *resource.ResourceClaimParameters, options printers.GenerateOptions) ([]metav1.TableRow, error) {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},
}
generatedFrom := ""
if obj.GeneratedFrom != nil {
generatedFrom = fmt.Sprintf("%s.%s %s", obj.GeneratedFrom.Kind, obj.GeneratedFrom.APIGroup, obj.GeneratedFrom.Name)
}
row.Cells = append(row.Cells, obj.Name, generatedFrom, translateTimestampSince(obj.CreationTimestamp))
return []metav1.TableRow{row}, nil
}
func printResourceClaimParametersList(list *resource.ResourceClaimParametersList, options printers.GenerateOptions) ([]metav1.TableRow, error) {
rows := make([]metav1.TableRow, 0, len(list.Items))
for i := range list.Items {
r, err := printResourceClaimParameters(&list.Items[i], options)
if err != nil {
return nil, err
}
rows = append(rows, r...)
}
return rows, nil
}
func printResourceClassParameters(obj *resource.ResourceClassParameters, options printers.GenerateOptions) ([]metav1.TableRow, error) {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},
}
generatedFrom := ""
if obj.GeneratedFrom != nil {
generatedFrom = fmt.Sprintf("%s.%s %s", obj.GeneratedFrom.Kind, obj.GeneratedFrom.APIGroup, obj.GeneratedFrom.Name)
}
row.Cells = append(row.Cells, obj.Name, generatedFrom, translateTimestampSince(obj.CreationTimestamp))
return []metav1.TableRow{row}, nil
}
func printResourceClassParametersList(list *resource.ResourceClassParametersList, options printers.GenerateOptions) ([]metav1.TableRow, error) {
rows := make([]metav1.TableRow, 0, len(list.Items))
for i := range list.Items {
r, err := printResourceClassParameters(&list.Items[i], options)
if err != nil {
return nil, err
}
rows = append(rows, r...)
}
return rows, nil
}
func printResourceSlice(obj *resource.ResourceSlice, options printers.GenerateOptions) ([]metav1.TableRow, error) {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},
}
row.Cells = append(row.Cells, obj.Name, obj.NodeName, obj.DriverName, translateTimestampSince(obj.CreationTimestamp))
row.Cells = append(row.Cells, obj.Name, obj.Spec.NodeName, obj.Spec.Driver, obj.Spec.Pool.Name, translateTimestampSince(obj.CreationTimestamp))
return []metav1.TableRow{row}, nil
}

View file

@ -24,25 +24,25 @@ import (
"k8s.io/kubernetes/pkg/printers"
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
"k8s.io/kubernetes/pkg/registry/resource/resourceclass"
"k8s.io/kubernetes/pkg/registry/resource/deviceclass"
)
// REST implements a RESTStorage for ResourceClass.
// REST implements a RESTStorage for DeviceClass.
type REST struct {
*genericregistry.Store
}
// NewREST returns a RESTStorage object that will work against ResourceClass.
// NewREST returns a RESTStorage object that will work against DeviceClass.
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) {
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &resource.ResourceClass{} },
NewListFunc: func() runtime.Object { return &resource.ResourceClassList{} },
DefaultQualifiedResource: resource.Resource("resourceclasses"),
SingularQualifiedResource: resource.Resource("resourceclass"),
NewFunc: func() runtime.Object { return &resource.DeviceClass{} },
NewListFunc: func() runtime.Object { return &resource.DeviceClassList{} },
DefaultQualifiedResource: resource.Resource("deviceclasses"),
SingularQualifiedResource: resource.Resource("deviceclass"),
CreateStrategy: resourceclass.Strategy,
UpdateStrategy: resourceclass.Strategy,
DeleteStrategy: resourceclass.Strategy,
CreateStrategy: deviceclass.Strategy,
UpdateStrategy: deviceclass.Strategy,
DeleteStrategy: deviceclass.Strategy,
ReturnDeletedObject: true,
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},

View file

@ -37,21 +37,20 @@ func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
StorageConfig: etcdStorage,
Decorator: generic.UndecoratedStorage,
DeleteCollectionWorkers: 1,
ResourcePrefix: "resourceclasses",
ResourcePrefix: "deviceclasses",
}
resourceClassStorage, err := NewREST(restOptions)
deviceClassStorage, err := NewREST(restOptions)
if err != nil {
t.Fatalf("unexpected error from REST storage: %v", err)
}
return resourceClassStorage, server
return deviceClassStorage, server
}
func validNewClass(name string) *resource.ResourceClass {
return &resource.ResourceClass{
func validNewClass(name string) *resource.DeviceClass {
return &resource.DeviceClass{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
DriverName: "cdi.example.com",
}
}
@ -60,13 +59,13 @@ func TestCreate(t *testing.T) {
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
resourceClass := validNewClass("foo")
resourceClass.ObjectMeta = metav1.ObjectMeta{GenerateName: "foo"}
deviceClass := validNewClass("foo")
deviceClass.ObjectMeta = metav1.ObjectMeta{GenerateName: "foo"}
test.TestCreate(
// valid
resourceClass,
deviceClass,
// invalid
&resource.ResourceClass{
&resource.DeviceClass{
ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"},
},
)
@ -82,14 +81,22 @@ func TestUpdate(t *testing.T) {
validNewClass("foo"),
// updateFunc
func(obj runtime.Object) runtime.Object {
object := obj.(*resource.ResourceClass)
object.ParametersRef = &resource.ResourceClassParametersReference{Kind: "cdiexample", Name: "some-name"}
object := obj.(*resource.DeviceClass)
object.Spec.Selectors = []resource.DeviceSelector{{
CEL: &resource.CELDeviceSelector{
Expression: "true",
},
}}
return object
},
//invalid update
func(obj runtime.Object) runtime.Object {
object := obj.(*resource.ResourceClass)
object.DriverName = ""
object := obj.(*resource.DeviceClass)
object.Spec.Selectors = []resource.DeviceSelector{{
CEL: &resource.CELDeviceSelector{
Expression: "?!#$",
},
}}
return object
},
)

View file

@ -0,0 +1,85 @@
/*
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 deviceclass
import (
"context"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/kubernetes/pkg/apis/resource/validation"
)
// deviceClassStrategy implements behavior for DeviceClass objects
type deviceClassStrategy struct {
runtime.ObjectTyper
names.NameGenerator
}
var Strategy = deviceClassStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
func (deviceClassStrategy) NamespaceScoped() bool {
return false
}
func (deviceClassStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
class := obj.(*resource.DeviceClass)
class.Generation = 1
}
func (deviceClassStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
deviceClass := obj.(*resource.DeviceClass)
return validation.ValidateDeviceClass(deviceClass)
}
func (deviceClassStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
return nil
}
func (deviceClassStrategy) Canonicalize(obj runtime.Object) {
}
func (deviceClassStrategy) AllowCreateOnUpdate() bool {
return false
}
func (deviceClassStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
class := obj.(*resource.DeviceClass)
oldClass := old.(*resource.DeviceClass)
// Any changes to the spec increment the generation number.
if !apiequality.Semantic.DeepEqual(oldClass.Spec, class.Spec) {
class.Generation = oldClass.Generation + 1
}
}
func (deviceClassStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
errorList := validation.ValidateDeviceClass(obj.(*resource.DeviceClass))
return append(errorList, validation.ValidateDeviceClassUpdate(obj.(*resource.DeviceClass), old.(*resource.DeviceClass))...)
}
func (deviceClassStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil
}
func (deviceClassStrategy) AllowUnconditionalUpdate() bool {
return true
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package resourceclass
package deviceclass
import (
"testing"
@ -24,28 +24,27 @@ import (
"k8s.io/kubernetes/pkg/apis/resource"
)
var resourceClass = &resource.ResourceClass{
var deviceClass = &resource.DeviceClass{
ObjectMeta: metav1.ObjectMeta{
Name: "valid-class",
},
DriverName: "resource-driver.example.com",
}
func TestClassStrategy(t *testing.T) {
if Strategy.NamespaceScoped() {
t.Errorf("ResourceClass must not be namespace scoped")
t.Errorf("DeviceClass must not be namespace scoped")
}
if Strategy.AllowCreateOnUpdate() {
t.Errorf("ResourceClass should not allow create on update")
t.Errorf("DeviceClass should not allow create on update")
}
}
func TestClassStrategyCreate(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
resourceClass := resourceClass.DeepCopy()
deviceClass := deviceClass.DeepCopy()
Strategy.PrepareForCreate(ctx, resourceClass)
errs := Strategy.Validate(ctx, resourceClass)
Strategy.PrepareForCreate(ctx, deviceClass)
errs := Strategy.Validate(ctx, deviceClass)
if len(errs) != 0 {
t.Errorf("unexpected error validating for create %v", errs)
}
@ -54,12 +53,12 @@ func TestClassStrategyCreate(t *testing.T) {
func TestClassStrategyUpdate(t *testing.T) {
t.Run("no-changes-okay", func(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
resourceClass := resourceClass.DeepCopy()
newClass := resourceClass.DeepCopy()
deviceClass := deviceClass.DeepCopy()
newClass := deviceClass.DeepCopy()
newClass.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newClass, resourceClass)
errs := Strategy.ValidateUpdate(ctx, newClass, resourceClass)
Strategy.PrepareForUpdate(ctx, newClass, deviceClass)
errs := Strategy.ValidateUpdate(ctx, newClass, deviceClass)
if len(errs) != 0 {
t.Errorf("unexpected validation errors: %v", errs)
}
@ -67,13 +66,13 @@ func TestClassStrategyUpdate(t *testing.T) {
t.Run("name-change-not-allowed", func(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
resourceClass := resourceClass.DeepCopy()
newClass := resourceClass.DeepCopy()
deviceClass := deviceClass.DeepCopy()
newClass := deviceClass.DeepCopy()
newClass.Name = "valid-class-2"
newClass.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newClass, resourceClass)
errs := Strategy.ValidateUpdate(ctx, newClass, resourceClass)
Strategy.PrepareForUpdate(ctx, newClass, deviceClass)
errs := Strategy.ValidateUpdate(ctx, newClass, deviceClass)
if len(errs) == 0 {
t.Errorf("expected a validation error")
}

View file

@ -56,10 +56,6 @@ func validNewClaim(name, ns string) *resource.ResourceClaim {
Name: name,
Namespace: ns,
},
Spec: resource.ResourceClaimSpec{
ResourceClassName: "example",
},
Status: resource.ResourceClaimStatus{},
}
return claim
}
@ -98,6 +94,12 @@ func TestUpdate(t *testing.T) {
object.Labels["foo"] = "bar"
return object
},
// invalid update
func(obj runtime.Object) runtime.Object {
object := obj.(*resource.ResourceClaim)
object.Name = "^%$#@#%"
return object
},
)
}
@ -163,7 +165,6 @@ func TestUpdateStatus(t *testing.T) {
}
claim := claimStart.DeepCopy()
claim.Status.DriverName = "some-driver.example.com"
claim.Status.Allocation = &resource.AllocationResult{}
_, _, err = statusStorage.Update(ctx, claim.Name, rest.DefaultUpdatedObjectInfo(claim), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{})
if err != nil {

View file

@ -69,7 +69,7 @@ func (resourceclaimStrategy) PrepareForCreate(ctx context.Context, obj runtime.O
func (resourceclaimStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
claim := obj.(*resource.ResourceClaim)
return validation.ValidateClaim(claim)
return validation.ValidateResourceClaim(claim)
}
func (resourceclaimStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
@ -92,8 +92,8 @@ func (resourceclaimStrategy) PrepareForUpdate(ctx context.Context, obj, old runt
func (resourceclaimStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
newClaim := obj.(*resource.ResourceClaim)
oldClaim := old.(*resource.ResourceClaim)
errorList := validation.ValidateClaim(newClaim)
return append(errorList, validation.ValidateClaimUpdate(newClaim, oldClaim)...)
errorList := validation.ValidateResourceClaim(newClaim)
return append(errorList, validation.ValidateResourceClaimUpdate(newClaim, oldClaim)...)
}
func (resourceclaimStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
@ -132,7 +132,7 @@ func (resourceclaimStatusStrategy) PrepareForUpdate(ctx context.Context, obj, ol
func (resourceclaimStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
newClaim := obj.(*resource.ResourceClaim)
oldClaim := old.(*resource.ResourceClaim)
return validation.ValidateClaimStatusUpdate(newClaim, oldClaim)
return validation.ValidateResourceClaimStatusUpdate(newClaim, oldClaim)
}
// WarningsOnUpdate returns warnings for the given update.

View file

@ -29,9 +29,6 @@ var resourceClaim = &resource.ResourceClaim{
Name: "valid-claim",
Namespace: "default",
},
Spec: resource.ResourceClaimSpec{
ResourceClassName: "valid-class",
},
}
func TestClaimStrategy(t *testing.T) {

View file

@ -1,57 +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 storage
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/kubernetes/pkg/printers"
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
"k8s.io/kubernetes/pkg/registry/resource/resourceclaimparameters"
)
// REST implements a RESTStorage for ResourceClaimParameters.
type REST struct {
*genericregistry.Store
}
// NewREST returns a RESTStorage object that will work against ResourceClaimParameters.
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) {
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &resource.ResourceClaimParameters{} },
NewListFunc: func() runtime.Object { return &resource.ResourceClaimParametersList{} },
PredicateFunc: resourceclaimparameters.Match,
DefaultQualifiedResource: resource.Resource("resourceclaimparameters"),
SingularQualifiedResource: resource.Resource("resourceclaimparameters"),
CreateStrategy: resourceclaimparameters.Strategy,
UpdateStrategy: resourceclaimparameters.Strategy,
DeleteStrategy: resourceclaimparameters.Strategy,
ReturnDeletedObject: true,
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
}
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: resourceclaimparameters.GetAttrs}
if err := store.CompleteWithOptions(options); err != nil {
return nil, err
}
return &REST{store}, nil
}

View file

@ -1,145 +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 storage
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
"k8s.io/kubernetes/pkg/apis/resource"
_ "k8s.io/kubernetes/pkg/apis/resource/install"
"k8s.io/kubernetes/pkg/registry/registrytest"
)
func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
etcdStorage, server := registrytest.NewEtcdStorage(t, resource.GroupName)
restOptions := generic.RESTOptions{
StorageConfig: etcdStorage,
Decorator: generic.UndecoratedStorage,
DeleteCollectionWorkers: 1,
ResourcePrefix: "resourceclaimparameters",
}
resourceClassStorage, err := NewREST(restOptions)
if err != nil {
t.Fatalf("unexpected error from REST storage: %v", err)
}
return resourceClassStorage, server
}
func validNewResourceClaimParameters(name string) *resource.ResourceClaimParameters {
return &resource.ResourceClaimParameters{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: metav1.NamespaceDefault,
},
}
}
func TestCreate(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store)
resourceClass := validNewResourceClaimParameters("foo")
resourceClass.ObjectMeta = metav1.ObjectMeta{}
test.TestCreate(
// valid
resourceClass,
// invalid
&resource.ResourceClaimParameters{
ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"},
},
)
}
func TestUpdate(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store)
test.TestUpdate(
// valid
validNewResourceClaimParameters("foo"),
// updateFunc
func(obj runtime.Object) runtime.Object {
object := obj.(*resource.ResourceClaimParameters)
object.Labels = map[string]string{"foo": "bar"}
return object
},
// invalid update
func(obj runtime.Object) runtime.Object {
object := obj.(*resource.ResourceClaimParameters)
object.Labels = map[string]string{"&$^^#%@": "1"}
return object
},
)
}
func TestDelete(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ReturnDeletedObject()
test.TestDelete(validNewResourceClaimParameters("foo"))
}
func TestGet(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store)
test.TestGet(validNewResourceClaimParameters("foo"))
}
func TestList(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store)
test.TestList(validNewResourceClaimParameters("foo"))
}
func TestWatch(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store)
test.TestWatch(
validNewResourceClaimParameters("foo"),
// matching labels
[]labels.Set{},
// not matching labels
[]labels.Set{
{"foo": "bar"},
},
// matching fields
[]fields.Set{
{"metadata.name": "foo"},
},
// not matching fields
[]fields.Set{
{"metadata.name": "bar"},
},
)
}

View file

@ -1,103 +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 resourceclaimparameters
import (
"context"
"errors"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/kubernetes/pkg/apis/resource/validation"
)
// resourceClaimParametersStrategy implements behavior for ResourceClaimParameters objects
type resourceClaimParametersStrategy struct {
runtime.ObjectTyper
names.NameGenerator
}
var Strategy = resourceClaimParametersStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
func (resourceClaimParametersStrategy) NamespaceScoped() bool {
return true
}
func (resourceClaimParametersStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
}
func (resourceClaimParametersStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
resourceClaimParameters := obj.(*resource.ResourceClaimParameters)
return validation.ValidateResourceClaimParameters(resourceClaimParameters)
}
func (resourceClaimParametersStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
return nil
}
func (resourceClaimParametersStrategy) Canonicalize(obj runtime.Object) {
}
func (resourceClaimParametersStrategy) AllowCreateOnUpdate() bool {
return false
}
func (resourceClaimParametersStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
}
func (resourceClaimParametersStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
return validation.ValidateResourceClaimParametersUpdate(obj.(*resource.ResourceClaimParameters), old.(*resource.ResourceClaimParameters))
}
func (resourceClaimParametersStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil
}
func (resourceClaimParametersStrategy) AllowUnconditionalUpdate() bool {
return true
}
// Match returns a generic matcher for a given label and field selector.
func Match(label labels.Selector, field fields.Selector) storage.SelectionPredicate {
return storage.SelectionPredicate{
Label: label,
Field: field,
GetAttrs: GetAttrs,
}
}
// GetAttrs returns labels and fields of a given object for filtering purposes.
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
parameters, ok := obj.(*resource.ResourceClaimParameters)
if !ok {
return nil, nil, errors.New("not a resourceclaim")
}
return labels.Set(parameters.Labels), toSelectableFields(parameters), nil
}
// toSelectableFields returns a field set that represents the object
func toSelectableFields(claim *resource.ResourceClaimParameters) fields.Set {
fields := generic.ObjectMetaFieldsSet(&claim.ObjectMeta, true)
return fields
}

View file

@ -1,81 +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 resourceclaimparameters
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/apis/resource"
)
var resourceClaimParameters = &resource.ResourceClaimParameters{
ObjectMeta: metav1.ObjectMeta{
Name: "valid",
Namespace: "ns",
},
}
func TestClassStrategy(t *testing.T) {
if !Strategy.NamespaceScoped() {
t.Errorf("ResourceClaimParameters must be namespace scoped")
}
if Strategy.AllowCreateOnUpdate() {
t.Errorf("ResourceClaimParameters should not allow create on update")
}
}
func TestClassStrategyCreate(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
resourceClaimParameters := resourceClaimParameters.DeepCopy()
Strategy.PrepareForCreate(ctx, resourceClaimParameters)
errs := Strategy.Validate(ctx, resourceClaimParameters)
if len(errs) != 0 {
t.Errorf("unexpected error validating for create %v", errs)
}
}
func TestClassStrategyUpdate(t *testing.T) {
t.Run("no-changes-okay", func(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
resourceClaimParameters := resourceClaimParameters.DeepCopy()
newObj := resourceClaimParameters.DeepCopy()
newObj.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newObj, resourceClaimParameters)
errs := Strategy.ValidateUpdate(ctx, newObj, resourceClaimParameters)
if len(errs) != 0 {
t.Errorf("unexpected validation errors: %v", errs)
}
})
t.Run("name-change-not-allowed", func(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
resourceClaimParameters := resourceClaimParameters.DeepCopy()
newObj := resourceClaimParameters.DeepCopy()
newObj.Name += "-2"
newObj.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newObj, resourceClaimParameters)
errs := Strategy.ValidateUpdate(ctx, newObj, resourceClaimParameters)
if len(errs) == 0 {
t.Errorf("expected a validation error")
}
})
}

View file

@ -52,11 +52,6 @@ func validNewClaimTemplate(name string) *resource.ResourceClaimTemplate {
Name: name,
Namespace: metav1.NamespaceDefault,
},
Spec: resource.ResourceClaimTemplateSpec{
Spec: resource.ResourceClaimSpec{
ResourceClassName: "valid-class",
},
},
}
}
@ -94,7 +89,7 @@ func TestUpdate(t *testing.T) {
//invalid update
func(obj runtime.Object) runtime.Object {
object := obj.(*resource.ResourceClaimTemplate)
object.Spec.Spec.ResourceClassName = ""
object.Name = "^%$#@#%"
return object
},
)

View file

@ -48,7 +48,7 @@ func (resourceClaimTemplateStrategy) PrepareForCreate(ctx context.Context, obj r
func (resourceClaimTemplateStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
resourceClaimTemplate := obj.(*resource.ResourceClaimTemplate)
return validation.ValidateClaimTemplate(resourceClaimTemplate)
return validation.ValidateResourceClaimTemplate(resourceClaimTemplate)
}
func (resourceClaimTemplateStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
@ -66,8 +66,8 @@ func (resourceClaimTemplateStrategy) PrepareForUpdate(ctx context.Context, obj,
}
func (resourceClaimTemplateStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
errorList := validation.ValidateClaimTemplate(obj.(*resource.ResourceClaimTemplate))
return append(errorList, validation.ValidateClaimTemplateUpdate(obj.(*resource.ResourceClaimTemplate), old.(*resource.ResourceClaimTemplate))...)
errorList := validation.ValidateResourceClaimTemplate(obj.(*resource.ResourceClaimTemplate))
return append(errorList, validation.ValidateResourceClaimTemplateUpdate(obj.(*resource.ResourceClaimTemplate), old.(*resource.ResourceClaimTemplate))...)
}
func (resourceClaimTemplateStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {

View file

@ -29,11 +29,6 @@ var resourceClaimTemplate = &resource.ResourceClaimTemplate{
Name: "valid-claim-template",
Namespace: "default",
},
Spec: resource.ResourceClaimTemplateSpec{
Spec: resource.ResourceClaimSpec{
ResourceClassName: "valid-class",
},
},
}
func TestClaimTemplateStrategy(t *testing.T) {

View file

@ -1,75 +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 resourceclass
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/kubernetes/pkg/apis/resource/validation"
)
// resourceClassStrategy implements behavior for ResourceClass objects
type resourceClassStrategy struct {
runtime.ObjectTyper
names.NameGenerator
}
var Strategy = resourceClassStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
func (resourceClassStrategy) NamespaceScoped() bool {
return false
}
func (resourceClassStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
}
func (resourceClassStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
resourceClass := obj.(*resource.ResourceClass)
return validation.ValidateClass(resourceClass)
}
func (resourceClassStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
return nil
}
func (resourceClassStrategy) Canonicalize(obj runtime.Object) {
}
func (resourceClassStrategy) AllowCreateOnUpdate() bool {
return false
}
func (resourceClassStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
}
func (resourceClassStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
errorList := validation.ValidateClass(obj.(*resource.ResourceClass))
return append(errorList, validation.ValidateClassUpdate(obj.(*resource.ResourceClass), old.(*resource.ResourceClass))...)
}
func (resourceClassStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil
}
func (resourceClassStrategy) AllowUnconditionalUpdate() bool {
return true
}

View file

@ -1,57 +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 storage
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/kubernetes/pkg/printers"
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
"k8s.io/kubernetes/pkg/registry/resource/resourceclassparameters"
)
// REST implements a RESTStorage for ResourceClassParameters.
type REST struct {
*genericregistry.Store
}
// NewREST returns a RESTStorage object that will work against ResourceClassParameters.
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) {
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &resource.ResourceClassParameters{} },
NewListFunc: func() runtime.Object { return &resource.ResourceClassParametersList{} },
PredicateFunc: resourceclassparameters.Match,
DefaultQualifiedResource: resource.Resource("resourceclassparameters"),
SingularQualifiedResource: resource.Resource("resourceclassparameters"),
CreateStrategy: resourceclassparameters.Strategy,
UpdateStrategy: resourceclassparameters.Strategy,
DeleteStrategy: resourceclassparameters.Strategy,
ReturnDeletedObject: true,
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
}
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: resourceclassparameters.GetAttrs}
if err := store.CompleteWithOptions(options); err != nil {
return nil, err
}
return &REST{store}, nil
}

View file

@ -1,145 +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 storage
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
"k8s.io/kubernetes/pkg/apis/resource"
_ "k8s.io/kubernetes/pkg/apis/resource/install"
"k8s.io/kubernetes/pkg/registry/registrytest"
)
func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
etcdStorage, server := registrytest.NewEtcdStorage(t, resource.GroupName)
restOptions := generic.RESTOptions{
StorageConfig: etcdStorage,
Decorator: generic.UndecoratedStorage,
DeleteCollectionWorkers: 1,
ResourcePrefix: "resourceclassparameters",
}
resourceClassStorage, err := NewREST(restOptions)
if err != nil {
t.Fatalf("unexpected error from REST storage: %v", err)
}
return resourceClassStorage, server
}
func validNewResourceClassParameters(name string) *resource.ResourceClassParameters {
return &resource.ResourceClassParameters{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: metav1.NamespaceDefault,
},
}
}
func TestCreate(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store)
resourceClass := validNewResourceClassParameters("foo")
resourceClass.ObjectMeta = metav1.ObjectMeta{}
test.TestCreate(
// valid
resourceClass,
// invalid
&resource.ResourceClassParameters{
ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"},
},
)
}
func TestUpdate(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store)
test.TestUpdate(
// valid
validNewResourceClassParameters("foo"),
// updateFunc
func(obj runtime.Object) runtime.Object {
object := obj.(*resource.ResourceClassParameters)
object.Labels = map[string]string{"foo": "bar"}
return object
},
// invalid update
func(obj runtime.Object) runtime.Object {
object := obj.(*resource.ResourceClassParameters)
object.Labels = map[string]string{"&$^^#%@": "1"}
return object
},
)
}
func TestDelete(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ReturnDeletedObject()
test.TestDelete(validNewResourceClassParameters("foo"))
}
func TestGet(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store)
test.TestGet(validNewResourceClassParameters("foo"))
}
func TestList(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store)
test.TestList(validNewResourceClassParameters("foo"))
}
func TestWatch(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store)
test.TestWatch(
validNewResourceClassParameters("foo"),
// matching labels
[]labels.Set{},
// not matching labels
[]labels.Set{
{"foo": "bar"},
},
// matching fields
[]fields.Set{
{"metadata.name": "foo"},
},
// not matching fields
[]fields.Set{
{"metadata.name": "bar"},
},
)
}

View file

@ -1,103 +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 resourceclassparameters
import (
"context"
"errors"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/kubernetes/pkg/apis/resource/validation"
)
// resourceClassParametersStrategy implements behavior for ResourceClassParameters objects
type resourceClassParametersStrategy struct {
runtime.ObjectTyper
names.NameGenerator
}
var Strategy = resourceClassParametersStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
func (resourceClassParametersStrategy) NamespaceScoped() bool {
return true
}
func (resourceClassParametersStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
}
func (resourceClassParametersStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
resourceClassParameters := obj.(*resource.ResourceClassParameters)
return validation.ValidateResourceClassParameters(resourceClassParameters)
}
func (resourceClassParametersStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
return nil
}
func (resourceClassParametersStrategy) Canonicalize(obj runtime.Object) {
}
func (resourceClassParametersStrategy) AllowCreateOnUpdate() bool {
return false
}
func (resourceClassParametersStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
}
func (resourceClassParametersStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
return validation.ValidateResourceClassParametersUpdate(obj.(*resource.ResourceClassParameters), old.(*resource.ResourceClassParameters))
}
func (resourceClassParametersStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil
}
func (resourceClassParametersStrategy) AllowUnconditionalUpdate() bool {
return true
}
// Match returns a generic matcher for a given label and field selector.
func Match(label labels.Selector, field fields.Selector) storage.SelectionPredicate {
return storage.SelectionPredicate{
Label: label,
Field: field,
GetAttrs: GetAttrs,
}
}
// GetAttrs returns labels and fields of a given object for filtering purposes.
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
parameters, ok := obj.(*resource.ResourceClassParameters)
if !ok {
return nil, nil, errors.New("not a resourceclassparameters")
}
return labels.Set(parameters.Labels), toSelectableFields(parameters), nil
}
// toSelectableFields returns a field set that represents the object
func toSelectableFields(class *resource.ResourceClassParameters) fields.Set {
fields := generic.ObjectMetaFieldsSet(&class.ObjectMeta, true)
return fields
}

View file

@ -1,81 +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 resourceclassparameters
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/apis/resource"
)
var resourceClassParameters = &resource.ResourceClassParameters{
ObjectMeta: metav1.ObjectMeta{
Name: "valid",
Namespace: "ns",
},
}
func TestClassStrategy(t *testing.T) {
if !Strategy.NamespaceScoped() {
t.Errorf("ResourceClassParameters must be namespace scoped")
}
if Strategy.AllowCreateOnUpdate() {
t.Errorf("ResourceClassParameters should not allow create on update")
}
}
func TestClassStrategyCreate(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
resourceClassParameters := resourceClassParameters.DeepCopy()
Strategy.PrepareForCreate(ctx, resourceClassParameters)
errs := Strategy.Validate(ctx, resourceClassParameters)
if len(errs) != 0 {
t.Errorf("unexpected error validating for create %v", errs)
}
}
func TestClassStrategyUpdate(t *testing.T) {
t.Run("no-changes-okay", func(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
resourceClassParameters := resourceClassParameters.DeepCopy()
newObj := resourceClassParameters.DeepCopy()
newObj.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newObj, resourceClassParameters)
errs := Strategy.ValidateUpdate(ctx, newObj, resourceClassParameters)
if len(errs) != 0 {
t.Errorf("unexpected validation errors: %v", errs)
}
})
t.Run("name-change-not-allowed", func(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
resourceClassParameters := resourceClassParameters.DeepCopy()
newObj := resourceClassParameters.DeepCopy()
newObj.Name += "-2"
newObj.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newObj, resourceClassParameters)
errs := Strategy.ValidateUpdate(ctx, newObj, resourceClassParameters)
if len(errs) == 0 {
t.Errorf("expected a validation error")
}
})
}

View file

@ -51,10 +51,13 @@ func validNewResourceSlice(name string) *resource.ResourceSlice {
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
NodeName: name,
DriverName: "cdi.example.com",
ResourceModel: resource.ResourceModel{
NamedResources: &resource.NamedResourcesResources{},
Spec: resource.ResourceSliceSpec{
NodeName: name,
Driver: "cdi.example.com",
Pool: resource.ResourcePool{
Name: "worker-1",
ResourceSliceCount: 1,
},
},
}
}
@ -93,7 +96,7 @@ func TestUpdate(t *testing.T) {
// invalid update
func(obj runtime.Object) runtime.Object {
object := obj.(*resource.ResourceSlice)
object.DriverName = ""
object.Spec.Driver = ""
return object
},
)

View file

@ -20,6 +20,7 @@ import (
"context"
"fmt"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
@ -46,6 +47,8 @@ func (resourceSliceStrategy) NamespaceScoped() bool {
}
func (resourceSliceStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
slice := obj.(*resource.ResourceSlice)
slice.Generation = 1
}
func (resourceSliceStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
@ -65,6 +68,13 @@ func (resourceSliceStrategy) AllowCreateOnUpdate() bool {
}
func (resourceSliceStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
slice := obj.(*resource.ResourceSlice)
oldSlice := old.(*resource.ResourceSlice)
// Any changes to the spec increment the generation number.
if !apiequality.Semantic.DeepEqual(oldSlice.Spec, slice.Spec) {
slice.Generation = oldSlice.Generation + 1
}
}
func (resourceSliceStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
@ -82,17 +92,17 @@ func (resourceSliceStrategy) AllowUnconditionalUpdate() bool {
var TriggerFunc = map[string]storage.IndexerFunc{
// Only one index is supported:
// https://github.com/kubernetes/kubernetes/blob/3aa8c59fec0bf339e67ca80ea7905c817baeca85/staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go#L346-L350
"nodeName": nodeNameTriggerFunc,
resource.ResourceSliceSelectorNodeName: nodeNameTriggerFunc,
}
func nodeNameTriggerFunc(obj runtime.Object) string {
return obj.(*resource.ResourceSlice).NodeName
return obj.(*resource.ResourceSlice).Spec.NodeName
}
// Indexers returns the indexers for ResourceSlice.
func Indexers() *cache.Indexers {
return &cache.Indexers{
storage.FieldIndex("nodeName"): nodeNameIndexFunc,
storage.FieldIndex(resource.ResourceSliceSelectorNodeName): nodeNameIndexFunc,
}
}
@ -101,7 +111,7 @@ func nodeNameIndexFunc(obj interface{}) ([]string, error) {
if !ok {
return nil, fmt.Errorf("not a ResourceSlice")
}
return []string{slice.NodeName}, nil
return []string{slice.Spec.NodeName}, nil
}
// GetAttrs returns labels and fields of a given object for filtering purposes.
@ -119,7 +129,7 @@ func Match(label labels.Selector, field fields.Selector) storage.SelectionPredic
Label: label,
Field: field,
GetAttrs: GetAttrs,
IndexFields: []string{"nodeName"},
IndexFields: []string{resource.ResourceSliceSelectorNodeName},
}
}
@ -131,8 +141,8 @@ func toSelectableFields(slice *resource.ResourceSlice) fields.Set {
// field here or the number of object-meta related fields changes, this should
// be adjusted.
fields := make(fields.Set, 3)
fields["nodeName"] = slice.NodeName
fields["driverName"] = slice.DriverName
fields[resource.ResourceSliceSelectorNodeName] = slice.Spec.NodeName
fields[resource.ResourceSliceSelectorDriver] = slice.Spec.Driver
// Adds one field.
return generic.AddObjectMetaFieldsSet(fields, &slice.ObjectMeta, false)

View file

@ -28,14 +28,17 @@ var slice = &resource.ResourceSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "valid-class",
},
NodeName: "valid-node-name",
DriverName: "testdriver.example.com",
ResourceModel: resource.ResourceModel{
NamedResources: &resource.NamedResourcesResources{},
Spec: resource.ResourceSliceSpec{
NodeName: "valid-node-name",
Driver: "testdriver.example.com",
Pool: resource.ResourcePool{
Name: "valid-pool-name",
ResourceSliceCount: 1,
},
},
}
func TestClassStrategy(t *testing.T) {
func TestResourceSliceStrategy(t *testing.T) {
if Strategy.NamespaceScoped() {
t.Errorf("ResourceSlice must not be namespace scoped")
}
@ -44,7 +47,7 @@ func TestClassStrategy(t *testing.T) {
}
}
func TestClassStrategyCreate(t *testing.T) {
func TestResourceSliceStrategyCreate(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
slice := slice.DeepCopy()
@ -55,15 +58,15 @@ func TestClassStrategyCreate(t *testing.T) {
}
}
func TestClassStrategyUpdate(t *testing.T) {
func TestResourceSliceStrategyUpdate(t *testing.T) {
t.Run("no-changes-okay", func(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
slice := slice.DeepCopy()
newClass := slice.DeepCopy()
newClass.ResourceVersion = "4"
newSlice := slice.DeepCopy()
newSlice.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newClass, slice)
errs := Strategy.ValidateUpdate(ctx, newClass, slice)
Strategy.PrepareForUpdate(ctx, newSlice, slice)
errs := Strategy.ValidateUpdate(ctx, newSlice, slice)
if len(errs) != 0 {
t.Errorf("unexpected validation errors: %v", errs)
}
@ -72,12 +75,12 @@ func TestClassStrategyUpdate(t *testing.T) {
t.Run("name-change-not-allowed", func(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
slice := slice.DeepCopy()
newClass := slice.DeepCopy()
newClass.Name = "valid-class-2"
newClass.ResourceVersion = "4"
newSlice := slice.DeepCopy()
newSlice.Name = "valid-slice-2"
newSlice.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newClass, slice)
errs := Strategy.ValidateUpdate(ctx, newClass, slice)
Strategy.PrepareForUpdate(ctx, newSlice, slice)
errs := Strategy.ValidateUpdate(ctx, newSlice, slice)
if len(errs) == 0 {
t.Errorf("expected a validation error")
}

View file

@ -24,12 +24,10 @@ import (
serverstorage "k8s.io/apiserver/pkg/server/storage"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/resource"
deviceclassstore "k8s.io/kubernetes/pkg/registry/resource/deviceclass/storage"
podschedulingcontextsstore "k8s.io/kubernetes/pkg/registry/resource/podschedulingcontext/storage"
resourceclaimstore "k8s.io/kubernetes/pkg/registry/resource/resourceclaim/storage"
resourceclaimparametersstore "k8s.io/kubernetes/pkg/registry/resource/resourceclaimparameters/storage"
resourceclaimtemplatestore "k8s.io/kubernetes/pkg/registry/resource/resourceclaimtemplate/storage"
resourceclassstore "k8s.io/kubernetes/pkg/registry/resource/resourceclass/storage"
resourceclassparametersstore "k8s.io/kubernetes/pkg/registry/resource/resourceclassparameters/storage"
resourceslicestore "k8s.io/kubernetes/pkg/registry/resource/resourceslice/storage"
)
@ -52,12 +50,12 @@ func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorag
func (p RESTStorageProvider) v1alpha3Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) {
storage := map[string]rest.Storage{}
if resource := "resourceclasses"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha3.SchemeGroupVersion.WithResource(resource)) {
resourceClassStorage, err := resourceclassstore.NewREST(restOptionsGetter)
if resource := "deviceclasses"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha3.SchemeGroupVersion.WithResource(resource)) {
deviceclassStorage, err := deviceclassstore.NewREST(restOptionsGetter)
if err != nil {
return nil, err
}
storage[resource] = resourceClassStorage
storage[resource] = deviceclassStorage
}
if resource := "resourceclaims"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha3.SchemeGroupVersion.WithResource(resource)) {
@ -86,22 +84,6 @@ func (p RESTStorageProvider) v1alpha3Storage(apiResourceConfigSource serverstora
storage[resource+"/status"] = podSchedulingStatusStorage
}
if resource := "resourceclaimparameters"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha3.SchemeGroupVersion.WithResource(resource)) {
resourceClaimParametersStorage, err := resourceclaimparametersstore.NewREST(restOptionsGetter)
if err != nil {
return nil, err
}
storage[resource] = resourceClaimParametersStorage
}
if resource := "resourceclassparameters"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha3.SchemeGroupVersion.WithResource(resource)) {
resourceClassParametersStorage, err := resourceclassparametersstore.NewREST(restOptionsGetter)
if err != nil {
return nil, err
}
storage[resource] = resourceClassParametersStorage
}
if resource := "resourceslices"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha3.SchemeGroupVersion.WithResource(resource)) {
resourceSliceStorage, err := resourceslicestore.NewREST(restOptionsGetter)
if err != nil {

View file

@ -34,7 +34,6 @@ import (
resourceapi "k8s.io/api/resource/v1alpha3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
@ -1363,13 +1362,6 @@ func setup(t *testing.T, nodes []*v1.Node, claims []*resourceapi.ResourceClaim,
reactor := createReactor(tc.client.Tracker())
tc.client.PrependReactor("*", "*", reactor)
// Quick-and-dirty workaround for fake client storing ResourceClassParameters and
// ResourceClaimParameters as "resourceclassparameterses" and "resourceclaimparameterses":
// intercept the correct LIST from the informers and reply to them with the incorrect
// LIST result.
tc.client.PrependReactor("list", "resourceclaimparameters", createListReactor(tc.client.Tracker(), "ResourceClaimParameters"))
tc.client.PrependReactor("list", "resourceclassparameters", createListReactor(tc.client.Tracker(), "ResourceClassParameters"))
tc.informerFactory = informers.NewSharedInformerFactory(tc.client, 0)
tc.claimAssumeCache = assumecache.NewAssumeCache(tCtx.Logger(), tc.informerFactory.Resource().V1alpha3().ResourceClaims().Informer(), "resource claim", "", nil)
opts := []runtime.Option{
@ -1485,17 +1477,6 @@ func createReactor(tracker cgotesting.ObjectTracker) func(action cgotesting.Acti
}
}
func createListReactor(tracker cgotesting.ObjectTracker, kind string) func(action cgotesting.Action) (handled bool, ret apiruntime.Object, err error) {
return func(action cgotesting.Action) (handled bool, ret apiruntime.Object, err error) {
// listAction := action.(cgotesting.ListAction)
gvr := action.GetResource()
ns := action.GetNamespace()
gvr.Resource += "es"
list, err := tracker.List(gvr, schema.GroupVersionKind{Group: gvr.Group, Version: gvr.Version, Kind: kind}, ns)
return true, list, err
}
}
func Test_isSchedulableAfterClaimChange(t *testing.T) {
testcases := map[string]struct {
pod *v1.Pod

View file

@ -54,20 +54,36 @@ type assumeCacheLister interface {
// with an unknown structured parameter model silently ignored. An error gets
// logged later when parameters required for a pod depend on such an unknown
// model.
func newResourceModel(logger klog.Logger, resourceSliceLister resourceSliceLister, claimAssumeCache assumeCacheLister, inFlightAllocations *sync.Map) (resources, error) {
model := make(resources)
func newResourceModel(logger klog.Logger, resourceSliceLister resourceSliceLister, claimAssumeCache assumeCacheLister, inFlightAllocations *sync.Map) (resourceMap, error) {
model := make(resourceMap)
slices, err := resourceSliceLister.List(labels.Everything())
if err != nil {
return nil, fmt.Errorf("list node resource slices: %w", err)
}
for _, slice := range slices {
if model[slice.NodeName] == nil {
model[slice.NodeName] = make(map[string]ResourceModels)
if slice.NamedResources == nil {
// Ignore unknown resource. We don't know what it is,
// so we cannot allocated anything depending on
// it. This is only an error if we actually see a claim
// which needs this unknown model.
continue
}
resource := model[slice.NodeName][slice.DriverName]
namedresourcesmodel.AddResources(&resource.NamedResources, slice.NamedResources)
model[slice.NodeName][slice.DriverName] = resource
instances := slice.NamedResources.Instances
if model[slice.NodeName] == nil {
model[slice.NodeName] = make(map[string]Resources)
}
resources := model[slice.NodeName][slice.DriverName]
resources.Instances = make([]Instance, 0, len(instances))
for i := range instances {
instance := Instance{
NodeName: slice.NodeName,
DriverName: slice.DriverName,
NamedResourcesInstance: &instances[i],
}
resources.Instances = append(resources.Instances, instance)
}
model[slice.NodeName][slice.DriverName] = resources
}
objs := claimAssumeCache.List(nil)
@ -90,12 +106,23 @@ func newResourceModel(logger klog.Logger, resourceSliceLister resourceSliceListe
continue
}
if model[structured.NodeName] == nil {
model[structured.NodeName] = make(map[string]ResourceModels)
model[structured.NodeName] = make(map[string]Resources)
}
resource := model[structured.NodeName][handle.DriverName]
resources := model[structured.NodeName][handle.DriverName]
for _, result := range structured.Results {
// Call AddAllocation for each known model. Each call itself needs to check for nil.
namedresourcesmodel.AddAllocation(&resource.NamedResources, result.NamedResources)
// Same as above: if we don't know the allocation result model, ignore it.
if result.NamedResources == nil {
continue
}
instanceName := result.NamedResources.Name
for i := range resources.Instances {
if resources.Instances[i].NamedResourcesInstance.Name == instanceName {
resources.Instances[i].Allocated = true
break
}
}
// It could be that we don't know the instance. That's okay,
// we simply ignore the allocation result.
}
}
}

View file

@ -663,7 +663,7 @@ func (p *Plugin) admitResourceSlice(nodeName string, a admission.Attributes) err
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
}
if slice.NodeName != nodeName {
if slice.Spec.NodeName != nodeName {
return admission.NewForbidden(a, errors.New("can only create ResourceSlice with the same NodeName as the requesting node"))
}
case admission.Delete:
@ -672,7 +672,7 @@ func (p *Plugin) admitResourceSlice(nodeName string, a admission.Attributes) err
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetOldObject()))
}
if slice.NodeName != nodeName {
if slice.Spec.NodeName != nodeName {
return admission.NewForbidden(a, errors.New("can only delete ResourceSlice with the same NodeName as the requesting node"))
}
}

View file

@ -1687,13 +1687,25 @@ func TestAdmitResourceSlice(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "something",
},
NodeName: nodename,
Spec: resourceapi.ResourceSliceSpec{
NodeName: nodename,
},
}
sliceOtherNode := &resourceapi.ResourceSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "something",
},
NodeName: nodename + "-other",
Spec: resourceapi.ResourceSliceSpec{
NodeName: nodename + "-other",
},
}
sliceNoNode := &resourceapi.ResourceSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "something",
},
Spec: resourceapi.ResourceSliceSpec{
NodeName: "",
},
}
tests := map[string]struct {
@ -1717,6 +1729,13 @@ func TestAdmitResourceSlice(t *testing.T) {
featureEnabled: true,
expectError: createErr,
},
"create disallowed, no node name, enabled": {
operation: admission.Create,
options: &metav1.CreateOptions{},
obj: sliceNoNode,
featureEnabled: true,
expectError: createErr,
},
"create allowed, disabled": {
operation: admission.Create,
options: &metav1.CreateOptions{},
@ -1731,6 +1750,13 @@ func TestAdmitResourceSlice(t *testing.T) {
featureEnabled: false,
expectError: createErr,
},
"create disallowed, no node name, disabled": {
operation: admission.Create,
options: &metav1.CreateOptions{},
obj: sliceNoNode,
featureEnabled: false,
expectError: createErr,
},
"update allowed, same node": {
operation: admission.Update,
options: &metav1.UpdateOptions{},
@ -1745,6 +1771,13 @@ func TestAdmitResourceSlice(t *testing.T) {
featureEnabled: true,
expectError: "",
},
"update allowed, no node": {
operation: admission.Update,
options: &metav1.UpdateOptions{},
obj: sliceNoNode,
featureEnabled: true,
expectError: "",
},
"delete allowed, enabled": {
operation: admission.Delete,
options: &metav1.DeleteOptions{},
@ -1759,6 +1792,13 @@ func TestAdmitResourceSlice(t *testing.T) {
featureEnabled: true,
expectError: deleteErr,
},
"delete disallowed, no node name, enabled": {
operation: admission.Delete,
options: &metav1.DeleteOptions{},
oldObj: sliceNoNode,
featureEnabled: true,
expectError: deleteErr,
},
"delete allowed, disabled": {
operation: admission.Delete,
options: &metav1.DeleteOptions{},
@ -1773,6 +1813,13 @@ func TestAdmitResourceSlice(t *testing.T) {
featureEnabled: false,
expectError: deleteErr,
},
"delete disallowed, no node name, disabled": {
operation: admission.Delete,
options: &metav1.DeleteOptions{},
oldObj: sliceNoNode,
featureEnabled: false,
expectError: deleteErr,
},
}
for name, test := range tests {

View file

@ -206,7 +206,7 @@ func (g *graphPopulator) addResourceSlice(obj interface{}) {
klog.Infof("unexpected type %T", obj)
return
}
g.graph.AddResourceSlice(slice.Name, slice.NodeName)
g.graph.AddResourceSlice(slice.Name, slice.Spec.NodeName)
}
func (g *graphPopulator) deleteResourceSlice(obj interface{}) {

View file

@ -338,7 +338,7 @@ func (r *NodeAuthorizer) authorizeResourceSlice(nodeName string, attrs authorize
// only allow a scoped fieldSelector
reqs, _ := attrs.GetFieldSelector()
for _, req := range reqs {
if req.Field == "nodeName" && req.Operator == selection.Equals && req.Value == nodeName {
if req.Field == resourceapi.ResourceSliceSelectorNodeName && req.Operator == selection.Equals && req.Value == nodeName {
return authorizer.DecisionAllow, "", nil
}
}

View file

@ -60,7 +60,7 @@ func TestNodeAuthorizer(t *testing.T) {
uniqueResourceClaimsPerPod: 1,
uniqueResourceClaimTemplatesPerPod: 1,
uniqueResourceClaimTemplatesWithClaimPerPod: 1,
nodeResourceCapacitiesPerNode: 2,
nodeResourceSlicesPerNode: 2,
}
nodes, pods, pvs, attachments, slices := generate(opts)
populate(g, nodes, pods, pvs, attachments, slices)
@ -386,7 +386,7 @@ func TestNodeAuthorizer(t *testing.T) {
},
{
name: "allowed filtered list ResourceSlices",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "resourceslices", APIGroup: "resource.k8s.io", FieldSelectorRequirements: mustParseFields("nodeName==node0")},
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "resourceslices", APIGroup: "resource.k8s.io", FieldSelectorRequirements: mustParseFields("spec.nodeName==node0")},
expect: authorizer.DecisionAllow,
},
{
@ -403,7 +403,7 @@ func TestNodeAuthorizer(t *testing.T) {
},
{
name: "allowed filtered watch ResourceSlices",
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "resourceslices", APIGroup: "resource.k8s.io", FieldSelectorRequirements: mustParseFields("nodeName==node0")},
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "resourceslices", APIGroup: "resource.k8s.io", FieldSelectorRequirements: mustParseFields("spec.nodeName==node0")},
expect: authorizer.DecisionAllow,
},
{
@ -880,7 +880,7 @@ type sampleDataOpts struct {
uniqueResourceClaimTemplatesPerPod int
uniqueResourceClaimTemplatesWithClaimPerPod int
nodeResourceCapacitiesPerNode int
nodeResourceSlicesPerNode int
}
func mustParseFields(s string) fields.Requirements {
@ -1191,7 +1191,7 @@ func generate(opts *sampleDataOpts) ([]*corev1.Node, []*corev1.Pod, []*corev1.Pe
pods := make([]*corev1.Pod, 0, opts.nodes*opts.podsPerNode)
pvs := make([]*corev1.PersistentVolume, 0, (opts.nodes*opts.podsPerNode*opts.uniquePVCsPerPod)+(opts.sharedPVCsPerPod*opts.namespaces))
attachments := make([]*storagev1.VolumeAttachment, 0, opts.nodes*opts.attachmentsPerNode)
slices := make([]*resourceapi.ResourceSlice, 0, opts.nodes*opts.nodeResourceCapacitiesPerNode)
slices := make([]*resourceapi.ResourceSlice, 0, opts.nodes*opts.nodeResourceSlicesPerNode)
rand.Seed(12345)
@ -1218,11 +1218,13 @@ func generate(opts *sampleDataOpts) ([]*corev1.Node, []*corev1.Pod, []*corev1.Pe
Spec: corev1.NodeSpec{},
})
for p := 0; p <= opts.nodeResourceCapacitiesPerNode; p++ {
for p := 0; p <= opts.nodeResourceSlicesPerNode; p++ {
name := fmt.Sprintf("slice%d-%s", p, nodeName)
slice := &resourceapi.ResourceSlice{
ObjectMeta: metav1.ObjectMeta{Name: name},
NodeName: nodeName,
Spec: resourceapi.ResourceSliceSpec{
NodeName: nodeName,
},
}
slices = append(slices, slice)
}

View file

@ -578,13 +578,13 @@ func ClusterRoles() []rbacv1.ClusterRole {
// Needed for dynamic resource allocation.
if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) {
kubeSchedulerRules = append(kubeSchedulerRules,
rbacv1helpers.NewRule(Read...).Groups(resourceGroup).Resources("resourceclasses").RuleOrDie(),
rbacv1helpers.NewRule(Read...).Groups(resourceGroup).Resources("deviceclasses").RuleOrDie(),
rbacv1helpers.NewRule(ReadUpdate...).Groups(resourceGroup).Resources("resourceclaims").RuleOrDie(),
rbacv1helpers.NewRule(ReadUpdate...).Groups(resourceGroup).Resources("resourceclaims/status").RuleOrDie(),
rbacv1helpers.NewRule(ReadWrite...).Groups(resourceGroup).Resources("podschedulingcontexts").RuleOrDie(),
rbacv1helpers.NewRule(Read...).Groups(resourceGroup).Resources("podschedulingcontexts/status").RuleOrDie(),
rbacv1helpers.NewRule(ReadUpdate...).Groups(legacyGroup).Resources("pods/finalizers").RuleOrDie(),
rbacv1helpers.NewRule(Read...).Groups(resourceGroup).Resources("resourceslices", "resourceclassparameters", "resourceclaimparameters").RuleOrDie(),
rbacv1helpers.NewRule(Read...).Groups(resourceGroup).Resources("resourceslices").RuleOrDie(),
)
}
roles = append(roles, rbacv1.ClusterRole{

File diff suppressed because it is too large Load diff

View file

@ -5001,6 +5001,13 @@ message ResourceClaim {
// the Pod where this field is used. It makes that resource available
// inside a container.
optional string name = 1;
// 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
optional string request = 2;
}
// ResourceFieldSelector represents container resources (cpu, memory) and their output format

View file

@ -2665,6 +2665,13 @@ type ResourceClaim struct {
// the Pod where this field is used. It makes that resource available
// inside a container.
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
// 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 `json:"request,omitempty" protobuf:"bytes,2,opt,name=request"`
}
const (

View file

@ -2103,8 +2103,9 @@ func (ReplicationControllerStatus) SwaggerDoc() map[string]string {
}
var map_ResourceClaim = map[string]string{
"": "ResourceClaim references one entry in PodSpec.ResourceClaims.",
"name": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.",
"": "ResourceClaim references one entry in PodSpec.ResourceClaims.",
"name": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.",
"request": "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.",
}
func (ResourceClaim) SwaggerDoc() map[string]string {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more