mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-05-28 04:04:39 -04:00
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:
parent
bcececadfb
commit
91d7882e86
306 changed files with 16480 additions and 26466 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
3809
api/openapi-spec/swagger.json
generated
3809
api/openapi-spec/swagger.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1951,11 +1951,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1463,11 +1463,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1465,11 +1465,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1617,11 +1617,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -750,11 +750,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -943,11 +943,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1652,11 +1652,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -979,11 +979,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -855,11 +855,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -875,11 +875,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1033,11 +1033,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -993,11 +993,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1479,11 +1479,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1480,11 +1480,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -969,11 +969,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1608,11 +1608,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1055,11 +1055,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -928,11 +928,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1047,11 +1047,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1320,11 +1320,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -846,11 +846,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2765,11 +2765,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -841,11 +841,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -953,11 +953,6 @@
|
|||
"group": "",
|
||||
"kind": "Status",
|
||||
"version": "v1"
|
||||
},
|
||||
{
|
||||
"group": "resource.k8s.io",
|
||||
"kind": "Status",
|
||||
"version": "v1alpha3"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"}: {},
|
||||
|
|
|
|||
|
|
@ -2455,6 +2455,13 @@ type ResourceClaim struct {
|
|||
// the Pod where this field is used. It makes that resource available
|
||||
// inside a container.
|
||||
Name string
|
||||
|
||||
// Request is the name chosen for a request in the referenced claim.
|
||||
// If empty, everything from the claim is made available, otherwise
|
||||
// only the result of this request.
|
||||
//
|
||||
// +optional
|
||||
Request string
|
||||
}
|
||||
|
||||
// Container represents a single container that is expected to be run on the host.
|
||||
|
|
|
|||
2
pkg/apis/core/v1/zz_generated.conversion.go
generated
2
pkg/apis/core/v1/zz_generated.conversion.go
generated
|
|
@ -7439,6 +7439,7 @@ func Convert_core_ReplicationControllerStatus_To_v1_ReplicationControllerStatus(
|
|||
|
||||
func autoConvert_v1_ResourceClaim_To_core_ResourceClaim(in *v1.ResourceClaim, out *core.ResourceClaim, s conversion.Scope) error {
|
||||
out.Name = in.Name
|
||||
out.Request = in.Request
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -7449,6 +7450,7 @@ func Convert_v1_ResourceClaim_To_core_ResourceClaim(in *v1.ResourceClaim, out *c
|
|||
|
||||
func autoConvert_core_ResourceClaim_To_v1_ResourceClaim(in *core.ResourceClaim, out *v1.ResourceClaim, s conversion.Scope) error {
|
||||
out.Name = in.Name
|
||||
out.Request = in.Request
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6757,9 +6757,35 @@ func validateResourceClaimNames(claims []core.ResourceClaim, podClaimNames sets.
|
|||
allErrs = append(allErrs, field.Required(fldPath.Index(i), ""))
|
||||
} else {
|
||||
if names.Has(name) {
|
||||
// All requests of that claim already referenced.
|
||||
allErrs = append(allErrs, field.Duplicate(fldPath.Index(i), name))
|
||||
} else {
|
||||
names.Insert(name)
|
||||
key := name
|
||||
if claim.Request != "" {
|
||||
allErrs = append(allErrs, ValidateDNS1123Label(claim.Request, fldPath.Index(i).Child("request"))...)
|
||||
key += "/" + claim.Request
|
||||
}
|
||||
if names.Has(key) {
|
||||
// The exact same entry was already referenced.
|
||||
allErrs = append(allErrs, field.Duplicate(fldPath.Index(i), key))
|
||||
} else if claim.Request == "" {
|
||||
// When referencing a claim, there's an
|
||||
// overlap when previously some request
|
||||
// in the claim was referenced. This
|
||||
// cannot be checked with a map lookup,
|
||||
// we need to iterate.
|
||||
for key := range names {
|
||||
index := strings.Index(key, "/")
|
||||
if index < 0 {
|
||||
continue
|
||||
}
|
||||
if key[0:index] == name {
|
||||
allErrs = append(allErrs, field.Duplicate(fldPath.Index(i), name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
names.Insert(key)
|
||||
}
|
||||
if !podClaimNames.Has(name) {
|
||||
// field.NotFound doesn't accept an
|
||||
|
|
|
|||
|
|
@ -23816,6 +23816,8 @@ func TestValidateDynamicResourceAllocation(t *testing.T) {
|
|||
shortPodName := &metav1.ObjectMeta{
|
||||
Name: "some-pod",
|
||||
}
|
||||
requestName := "req-0"
|
||||
anotherRequestName := "req-1"
|
||||
goodClaimTemplate := podtest.MakePod("",
|
||||
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim-template"}}}))),
|
||||
podtest.SetRestartPolicy(core.RestartPolicyAlways),
|
||||
|
|
@ -23848,6 +23850,26 @@ func TestValidateDynamicResourceAllocation(t *testing.T) {
|
|||
ResourceClaimName: &externalClaimName,
|
||||
}),
|
||||
),
|
||||
"multiple claims with requests": podtest.MakePod("",
|
||||
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim", Request: requestName}, {Name: "another-claim", Request: requestName}}}))),
|
||||
podtest.SetResourceClaims(
|
||||
core.PodResourceClaim{
|
||||
Name: "my-claim",
|
||||
ResourceClaimName: &externalClaimName,
|
||||
},
|
||||
core.PodResourceClaim{
|
||||
Name: "another-claim",
|
||||
ResourceClaimName: &externalClaimName,
|
||||
}),
|
||||
),
|
||||
"single claim with requests": podtest.MakePod("",
|
||||
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim", Request: requestName}, {Name: "my-claim", Request: anotherRequestName}}}))),
|
||||
podtest.SetResourceClaims(
|
||||
core.PodResourceClaim{
|
||||
Name: "my-claim",
|
||||
ResourceClaimName: &externalClaimName,
|
||||
}),
|
||||
),
|
||||
"init container": podtest.MakePod("",
|
||||
podtest.SetInitContainers(podtest.MakeContainer("ctr-init", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}}}))),
|
||||
podtest.SetResourceClaims(core.PodResourceClaim{
|
||||
|
|
@ -23928,6 +23950,34 @@ func TestValidateDynamicResourceAllocation(t *testing.T) {
|
|||
ResourceClaimName: &externalClaimName,
|
||||
}),
|
||||
),
|
||||
"pod claim name duplicates without and with request": podtest.MakePod("",
|
||||
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}, {Name: "my-claim", Request: "req-0"}}}))),
|
||||
podtest.SetResourceClaims(core.PodResourceClaim{
|
||||
Name: "my-claim",
|
||||
ResourceClaimName: &externalClaimName,
|
||||
}),
|
||||
),
|
||||
"pod claim name duplicates with and without request": podtest.MakePod("",
|
||||
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim", Request: "req-0"}, {Name: "my-claim"}}}))),
|
||||
podtest.SetResourceClaims(core.PodResourceClaim{
|
||||
Name: "my-claim",
|
||||
ResourceClaimName: &externalClaimName,
|
||||
}),
|
||||
),
|
||||
"pod claim name duplicates with requests": podtest.MakePod("",
|
||||
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim", Request: "req-0"}, {Name: "my-claim", Request: "req-0"}}}))),
|
||||
podtest.SetResourceClaims(core.PodResourceClaim{
|
||||
Name: "my-claim",
|
||||
ResourceClaimName: &externalClaimName,
|
||||
}),
|
||||
),
|
||||
"bad request name": podtest.MakePod("",
|
||||
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim", Request: "*$@%^"}}}))),
|
||||
podtest.SetResourceClaims(core.PodResourceClaim{
|
||||
Name: "my-claim",
|
||||
ResourceClaimName: &externalClaimName,
|
||||
}),
|
||||
),
|
||||
"no claims defined": podtest.MakePod("",
|
||||
podtest.SetContainers(podtest.MakeContainer("ctr", podtest.SetContainerResources(core.ResourceRequirements{Claims: []core.ResourceClaim{{Name: "my-claim"}}}))),
|
||||
podtest.SetRestartPolicy(core.RestartPolicyAlways),
|
||||
|
|
|
|||
|
|
@ -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"}`)}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package resource
|
||||
|
||||
import "k8s.io/apimachinery/pkg/api/resource"
|
||||
|
||||
// NamedResourcesResources is used in ResourceModel.
|
||||
type NamedResourcesResources struct {
|
||||
// The list of all individual resources instances currently available.
|
||||
Instances []NamedResourcesInstance
|
||||
}
|
||||
|
||||
// NamedResourcesInstance represents one individual hardware instance that can be selected based
|
||||
// on its attributes.
|
||||
type NamedResourcesInstance struct {
|
||||
// Name is unique identifier among all resource instances managed by
|
||||
// the driver on the node. It must be a DNS subdomain.
|
||||
Name string
|
||||
|
||||
// Attributes defines the attributes of this resource instance.
|
||||
// The name of each attribute must be unique.
|
||||
Attributes []NamedResourcesAttribute
|
||||
}
|
||||
|
||||
// NamedResourcesAttribute is a combination of an attribute name and its value.
|
||||
type NamedResourcesAttribute struct {
|
||||
// Name is unique identifier among all resource instances managed by
|
||||
// the driver on the node. It must be a DNS subdomain.
|
||||
Name string
|
||||
|
||||
NamedResourcesAttributeValue
|
||||
}
|
||||
|
||||
// NamedResourcesAttributeValue must have one and only one field set.
|
||||
type NamedResourcesAttributeValue struct {
|
||||
// QuantityValue is a quantity.
|
||||
QuantityValue *resource.Quantity
|
||||
// BoolValue is a true/false value.
|
||||
BoolValue *bool
|
||||
// IntValue is a 64-bit integer.
|
||||
IntValue *int64
|
||||
// IntSliceValue is an array of 64-bit integers.
|
||||
IntSliceValue *NamedResourcesIntSlice
|
||||
// StringValue is a string.
|
||||
StringValue *string
|
||||
// StringSliceValue is an array of strings.
|
||||
StringSliceValue *NamedResourcesStringSlice
|
||||
// VersionValue is a semantic version according to semver.org spec 2.0.0.
|
||||
VersionValue *string
|
||||
}
|
||||
|
||||
// NamedResourcesIntSlice contains a slice of 64-bit integers.
|
||||
type NamedResourcesIntSlice struct {
|
||||
// Ints is the slice of 64-bit integers.
|
||||
Ints []int64
|
||||
}
|
||||
|
||||
// NamedResourcesStringSlice contains a slice of strings.
|
||||
type NamedResourcesStringSlice struct {
|
||||
// Strings is the slice of strings.
|
||||
Strings []string
|
||||
}
|
||||
|
||||
// NamedResourcesRequest is used in ResourceRequestModel.
|
||||
type NamedResourcesRequest struct {
|
||||
// Selector is a CEL expression which must evaluate to true if a
|
||||
// resource instance is suitable. The language is as defined in
|
||||
// https://kubernetes.io/docs/reference/using-api/cel/
|
||||
//
|
||||
// In addition, for each type NamedResourcesin AttributeValue there is a map that
|
||||
// resolves to the corresponding value of the instance under evaluation.
|
||||
// For example:
|
||||
//
|
||||
// attributes.quantity["a"].isGreaterThan(quantity("0")) &&
|
||||
// attributes.stringslice["b"].isSorted()
|
||||
Selector string
|
||||
}
|
||||
|
||||
// NamedResourcesFilter is used in ResourceFilterModel.
|
||||
type NamedResourcesFilter struct {
|
||||
// Selector is a CEL expression which must evaluate to true if a
|
||||
// resource instance is suitable. The language is as defined in
|
||||
// https://kubernetes.io/docs/reference/using-api/cel/
|
||||
//
|
||||
// In addition, for each type in NamedResourcesAttributeValue there is a map that
|
||||
// resolves to the corresponding value of the instance under evaluation.
|
||||
// For example:
|
||||
//
|
||||
// attributes.quantity["a"].isGreaterThan(quantity("0")) &&
|
||||
// attributes.stringslice["b"].isSorted()
|
||||
Selector string
|
||||
}
|
||||
|
||||
// NamedResourcesAllocationResult is used in AllocationResultModel.
|
||||
type NamedResourcesAllocationResult struct {
|
||||
// Name is the name of the selected resource instance.
|
||||
Name string
|
||||
}
|
||||
|
|
@ -52,8 +52,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
|||
return err
|
||||
}
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&ResourceClass{},
|
||||
&ResourceClassList{},
|
||||
&DeviceClass{},
|
||||
&DeviceClassList{},
|
||||
&ResourceClaim{},
|
||||
&ResourceClaimList{},
|
||||
&ResourceClaimTemplate{},
|
||||
|
|
@ -62,10 +62,6 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
|||
&PodSchedulingContextList{},
|
||||
&ResourceSlice{},
|
||||
&ResourceSliceList{},
|
||||
&ResourceClaimParameters{},
|
||||
&ResourceClaimParametersList{},
|
||||
&ResourceClassParameters{},
|
||||
&ResourceClassParametersList{},
|
||||
)
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1,178 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
namedresourcescel "k8s.io/dynamic-resource-allocation/structured/namedresources/cel"
|
||||
corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
||||
"k8s.io/kubernetes/pkg/apis/resource"
|
||||
)
|
||||
|
||||
var (
|
||||
validateInstanceName = corevalidation.ValidateDNS1123Subdomain
|
||||
validateAttributeName = corevalidation.ValidateDNS1123Subdomain
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
// StoredExpressions must be true if and only if validating CEL
|
||||
// expressions that were already stored persistently. This makes
|
||||
// validation more permissive by enabling CEL definitions that are not
|
||||
// valid yet for new expressions.
|
||||
StoredExpressions bool
|
||||
}
|
||||
|
||||
func ValidateResources(resources *resource.NamedResourcesResources, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := validateInstances(resources.Instances, fldPath.Child("instances"))
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func validateInstances(instances []resource.NamedResourcesInstance, fldPath *field.Path) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
instanceNames := sets.New[string]()
|
||||
for i, instance := range instances {
|
||||
idxPath := fldPath.Index(i)
|
||||
instanceName := instance.Name
|
||||
allErrs = append(allErrs, validateInstanceName(instanceName, idxPath.Child("name"))...)
|
||||
if instanceNames.Has(instanceName) {
|
||||
allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), instanceName))
|
||||
} else {
|
||||
instanceNames.Insert(instanceName)
|
||||
}
|
||||
allErrs = append(allErrs, validateAttributes(instance.Attributes, idxPath.Child("attributes"))...)
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
var (
|
||||
numericIdentifier = `(0|[1-9]\d*)`
|
||||
|
||||
preReleaseIdentifier = `(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)`
|
||||
|
||||
buildIdentifier = `[0-9a-zA-Z-]+`
|
||||
|
||||
semverRe = regexp.MustCompile(`^` +
|
||||
|
||||
// dot-separated version segments (e.g. 1.2.3)
|
||||
numericIdentifier + `\.` + numericIdentifier + `\.` + numericIdentifier +
|
||||
|
||||
// optional dot-separated prerelease segments (e.g. -alpha.PRERELEASE.1)
|
||||
`(-` + preReleaseIdentifier + `(\.` + preReleaseIdentifier + `)*)?` +
|
||||
|
||||
// optional dot-separated build identifier segments (e.g. +build.id.20240305)
|
||||
`(\+` + buildIdentifier + `(\.` + buildIdentifier + `)*)?` +
|
||||
|
||||
`$`)
|
||||
)
|
||||
|
||||
func validateAttributes(attributes []resource.NamedResourcesAttribute, fldPath *field.Path) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
attributeNames := sets.New[string]()
|
||||
for i, attribute := range attributes {
|
||||
idxPath := fldPath.Index(i)
|
||||
attributeName := attribute.Name
|
||||
allErrs = append(allErrs, validateAttributeName(attributeName, idxPath.Child("name"))...)
|
||||
if attributeNames.Has(attributeName) {
|
||||
allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), attributeName))
|
||||
} else {
|
||||
attributeNames.Insert(attributeName)
|
||||
}
|
||||
|
||||
entries := sets.New[string]()
|
||||
if attribute.QuantityValue != nil {
|
||||
entries.Insert("quantity")
|
||||
}
|
||||
if attribute.BoolValue != nil {
|
||||
entries.Insert("bool")
|
||||
}
|
||||
if attribute.IntValue != nil {
|
||||
entries.Insert("int")
|
||||
}
|
||||
if attribute.IntSliceValue != nil {
|
||||
entries.Insert("intSlice")
|
||||
}
|
||||
if attribute.StringValue != nil {
|
||||
entries.Insert("string")
|
||||
}
|
||||
if attribute.StringSliceValue != nil {
|
||||
entries.Insert("stringSlice")
|
||||
}
|
||||
if attribute.VersionValue != nil {
|
||||
entries.Insert("version")
|
||||
if !semverRe.MatchString(*attribute.VersionValue) {
|
||||
allErrs = append(allErrs, field.Invalid(idxPath.Child("version"), *attribute.VersionValue, "must be a string compatible with semver.org spec 2.0.0"))
|
||||
}
|
||||
}
|
||||
|
||||
switch len(entries) {
|
||||
case 0:
|
||||
allErrs = append(allErrs, field.Required(idxPath, "exactly one value must be set"))
|
||||
case 1:
|
||||
// Okay.
|
||||
default:
|
||||
allErrs = append(allErrs, field.Invalid(idxPath, sets.List(entries), "exactly one field must be set, not several"))
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func ValidateRequest(opts Options, request *resource.NamedResourcesRequest, fldPath *field.Path) field.ErrorList {
|
||||
return validateSelector(opts, request.Selector, fldPath.Child("selector"))
|
||||
}
|
||||
|
||||
func ValidateFilter(opts Options, filter *resource.NamedResourcesFilter, fldPath *field.Path) field.ErrorList {
|
||||
return validateSelector(opts, filter.Selector, fldPath.Child("selector"))
|
||||
}
|
||||
|
||||
func validateSelector(opts Options, selector string, fldPath *field.Path) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
if selector == "" {
|
||||
allErrs = append(allErrs, field.Required(fldPath, ""))
|
||||
} else {
|
||||
envType := environment.NewExpressions
|
||||
if opts.StoredExpressions {
|
||||
envType = environment.StoredExpressions
|
||||
}
|
||||
result := namedresourcescel.GetCompiler().CompileCELExpression(selector, envType)
|
||||
if result.Error != nil {
|
||||
allErrs = append(allErrs, convertCELErrorToValidationError(fldPath, selector, result.Error))
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func convertCELErrorToValidationError(fldPath *field.Path, expression string, err *cel.Error) *field.Error {
|
||||
switch err.Type {
|
||||
case cel.ErrorTypeRequired:
|
||||
return field.Required(fldPath, err.Detail)
|
||||
case cel.ErrorTypeInvalid:
|
||||
return field.Invalid(fldPath, expression, err.Detail)
|
||||
case cel.ErrorTypeInternal:
|
||||
return field.InternalError(fldPath, err)
|
||||
}
|
||||
return field.InternalError(fldPath, fmt.Errorf("unsupported error type: %w", err))
|
||||
}
|
||||
|
||||
func ValidateAllocationResult(result *resource.NamedResourcesAllocationResult, fldPath *field.Path) field.ErrorList {
|
||||
return validateInstanceName(result.Name, fldPath.Child("name"))
|
||||
}
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
resourceapi "k8s.io/kubernetes/pkg/apis/resource"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func testResources(instances []resourceapi.NamedResourcesInstance) *resourceapi.NamedResourcesResources {
|
||||
resources := &resourceapi.NamedResourcesResources{
|
||||
Instances: instances,
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
func TestValidateResources(t *testing.T) {
|
||||
goodName := "foo"
|
||||
badName := "!@#$%^"
|
||||
quantity := resource.MustParse("1")
|
||||
|
||||
scenarios := map[string]struct {
|
||||
resources *resourceapi.NamedResourcesResources
|
||||
wantFailures field.ErrorList
|
||||
}{
|
||||
"empty": {
|
||||
resources: testResources(nil),
|
||||
},
|
||||
"good": {
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName}}),
|
||||
},
|
||||
"bad-name": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: badName}}),
|
||||
},
|
||||
"duplicate-name": {
|
||||
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("instances").Index(1).Child("name"), goodName)},
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName}, {Name: goodName}}),
|
||||
},
|
||||
"quantity": {
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{QuantityValue: &quantity}}}}}),
|
||||
},
|
||||
"bool": {
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{BoolValue: ptr.To(true)}}}}}),
|
||||
},
|
||||
"int": {
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{IntValue: ptr.To(int64(1))}}}}}),
|
||||
},
|
||||
"int-slice": {
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{IntSliceValue: &resourceapi.NamedResourcesIntSlice{Ints: []int64{1, 2, 3}}}}}}}),
|
||||
},
|
||||
"string": {
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{StringValue: ptr.To("hello")}}}}}),
|
||||
},
|
||||
"string-slice": {
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{StringSliceValue: &resourceapi.NamedResourcesStringSlice{Strings: []string{"hello"}}}}}}}),
|
||||
},
|
||||
"version-okay": {
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0")}}}}}),
|
||||
},
|
||||
"version-beta": {
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta")}}}}}),
|
||||
},
|
||||
"version-beta-1": {
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta.1")}}}}}),
|
||||
},
|
||||
"version-build": {
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0+build")}}}}}),
|
||||
},
|
||||
"version-build-1": {
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0+build.1")}}}}}),
|
||||
},
|
||||
"version-beta-1-build-1": {
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta.1+build.1")}}}}}),
|
||||
},
|
||||
"version-bad": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.0", "must be a string compatible with semver.org spec 2.0.0")},
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0")}}}}}),
|
||||
},
|
||||
"version-bad-leading-zeros": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "01.0.0", "must be a string compatible with semver.org spec 2.0.0")},
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("01.0.0")}}}}}),
|
||||
},
|
||||
"version-bad-leading-zeros-middle": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.00.0", "must be a string compatible with semver.org spec 2.0.0")},
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.00.0")}}}}}),
|
||||
},
|
||||
"version-bad-leading-zeros-end": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.0.00", "must be a string compatible with semver.org spec 2.0.0")},
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.00")}}}}}),
|
||||
},
|
||||
"version-bad-spaces": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), " 1.0.0 ", "must be a string compatible with semver.org spec 2.0.0")},
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To(" 1.0.0 ")}}}}}),
|
||||
},
|
||||
"empty-attribute": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("instances").Index(0).Child("attributes").Index(0), "exactly one value must be set")},
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName}}}}),
|
||||
},
|
||||
"duplicate-value": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0), []string{"bool", "int"}, "exactly one field must be set, not several")},
|
||||
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{BoolValue: ptr.To(true), IntValue: ptr.To(int64(1))}}}}}),
|
||||
},
|
||||
}
|
||||
|
||||
for name, scenario := range scenarios {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
errs := ValidateResources(scenario.resources, nil)
|
||||
assert.Equal(t, scenario.wantFailures, errs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSelector(t *testing.T) {
|
||||
scenarios := map[string]struct {
|
||||
selector string
|
||||
wantFailures field.ErrorList
|
||||
}{
|
||||
"okay": {
|
||||
selector: "true",
|
||||
},
|
||||
"empty": {
|
||||
selector: "",
|
||||
wantFailures: field.ErrorList{field.Required(nil, "")},
|
||||
},
|
||||
"undefined": {
|
||||
selector: "nosuchvar",
|
||||
wantFailures: field.ErrorList{field.Invalid(nil, "nosuchvar", "compilation failed: ERROR: <input>:1:1: undeclared reference to 'nosuchvar' (in container '')\n | nosuchvar\n | ^")},
|
||||
},
|
||||
"wrong-type": {
|
||||
selector: "1",
|
||||
wantFailures: field.ErrorList{field.Invalid(nil, "1", "must evaluate to bool")},
|
||||
},
|
||||
"quantity": {
|
||||
selector: `attributes.quantity["name"].isGreaterThan(quantity("0"))`,
|
||||
},
|
||||
"bool": {
|
||||
selector: `attributes.bool["name"]`,
|
||||
},
|
||||
"int": {
|
||||
selector: `attributes.int["name"] > 0`,
|
||||
},
|
||||
"intslice": {
|
||||
selector: `attributes.intslice["name"].isSorted()`,
|
||||
},
|
||||
"string": {
|
||||
selector: `attributes.string["name"] == "fish"`,
|
||||
},
|
||||
"stringslice": {
|
||||
selector: `attributes.stringslice["name"].isSorted()`,
|
||||
},
|
||||
"version": {
|
||||
selector: `attributes.version["name"].isGreaterThan(semver("1.0.0"))`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, scenario := range scenarios {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// At the moment, there's no difference between stored and new expressions.
|
||||
// This uses the stricter validation.
|
||||
opts := Options{
|
||||
StoredExpressions: false,
|
||||
}
|
||||
errs := validateSelector(opts, scenario.selector, nil)
|
||||
assert.Equal(t, scenario.wantFailures, errs)
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
86
pkg/apis/resource/v1alpha3/defaults_test.go
Normal file
86
pkg/apis/resource/v1alpha3/defaults_test.go
Normal 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
|
||||
}
|
||||
1416
pkg/apis/resource/v1alpha3/zz_generated.conversion.go
generated
1416
pkg/apis/resource/v1alpha3/zz_generated.conversion.go
generated
File diff suppressed because it is too large
Load diff
35
pkg/apis/resource/v1alpha3/zz_generated.defaults.go
generated
35
pkg/apis/resource/v1alpha3/zz_generated.defaults.go
generated
|
|
@ -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
247
pkg/apis/resource/validation/validation_deviceclass_test.go
Normal file
247
pkg/apis/resource/validation/validation_deviceclass_test.go
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/resource"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func testClass(name string) *resource.DeviceClass {
|
||||
return &resource.DeviceClass{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClass(t *testing.T) {
|
||||
goodName := "foo"
|
||||
now := metav1.Now()
|
||||
badName := "!@#$%^"
|
||||
badValue := "spaces not allowed"
|
||||
|
||||
scenarios := map[string]struct {
|
||||
class *resource.DeviceClass
|
||||
wantFailures field.ErrorList
|
||||
}{
|
||||
"good-class": {
|
||||
class: testClass(goodName),
|
||||
},
|
||||
"missing-name": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
|
||||
class: testClass(""),
|
||||
},
|
||||
"bad-name": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
class: testClass(badName),
|
||||
},
|
||||
"generate-name": {
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.GenerateName = "pvc-"
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"uid": {
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"resource-version": {
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.ResourceVersion = "1"
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"generation": {
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.Generation = 100
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"creation-timestamp": {
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.CreationTimestamp = now
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"deletion-grace-period-seconds": {
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.DeletionGracePeriodSeconds = ptr.To(int64(10))
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"owner-references": {
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "pod",
|
||||
Name: "foo",
|
||||
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
|
||||
},
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"finalizers": {
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.Finalizers = []string{
|
||||
"example.com/foo",
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"managed-fields": {
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.ManagedFields = []metav1.ManagedFieldsEntry{
|
||||
{
|
||||
FieldsType: "FieldsV1",
|
||||
Operation: "Apply",
|
||||
APIVersion: "apps/v1",
|
||||
Manager: "foo",
|
||||
},
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"good-labels": {
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.Labels = map[string]string{
|
||||
"apps.kubernetes.io/name": "test",
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"bad-labels": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.Labels = map[string]string{
|
||||
"hello-world": badValue,
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"good-annotations": {
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.Annotations = map[string]string{
|
||||
"foo": "bar",
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"bad-annotations": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.Annotations = map[string]string{
|
||||
badName: "hello world",
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"invalid-node-selector": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("suitableNodes", "nodeSelectorTerms"), "must have at least one node selector term")},
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.Spec.SuitableNodes = &core.NodeSelector{
|
||||
// Must not be empty.
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"valid-node-selector": {
|
||||
class: func() *resource.DeviceClass {
|
||||
class := testClass(goodName)
|
||||
class.Spec.SuitableNodes = &core.NodeSelector{
|
||||
NodeSelectorTerms: []core.NodeSelectorTerm{{
|
||||
MatchExpressions: []core.NodeSelectorRequirement{{
|
||||
Key: "foo",
|
||||
Operator: core.NodeSelectorOpDoesNotExist,
|
||||
}},
|
||||
}},
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for name, scenario := range scenarios {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
errs := ValidateDeviceClass(scenario.class)
|
||||
assert.Equal(t, scenario.wantFailures, errs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClassUpdate(t *testing.T) {
|
||||
validClass := testClass(goodName)
|
||||
|
||||
scenarios := map[string]struct {
|
||||
oldClass *resource.DeviceClass
|
||||
update func(class *resource.DeviceClass) *resource.DeviceClass
|
||||
wantFailures field.ErrorList
|
||||
}{
|
||||
"valid-no-op-update": {
|
||||
oldClass: validClass,
|
||||
update: func(class *resource.DeviceClass) *resource.DeviceClass { return class },
|
||||
},
|
||||
"update-node-selector": {
|
||||
oldClass: validClass,
|
||||
update: func(class *resource.DeviceClass) *resource.DeviceClass {
|
||||
class = class.DeepCopy()
|
||||
class.Spec.SuitableNodes = &core.NodeSelector{
|
||||
NodeSelectorTerms: []core.NodeSelectorTerm{{
|
||||
MatchExpressions: []core.NodeSelectorRequirement{{
|
||||
Key: "foo",
|
||||
Operator: core.NodeSelectorOpDoesNotExist,
|
||||
}},
|
||||
}},
|
||||
}
|
||||
return class
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, scenario := range scenarios {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
scenario.oldClass.ResourceVersion = "1"
|
||||
errs := ValidateDeviceClassUpdate(scenario.update(scenario.oldClass.DeepCopy()), scenario.oldClass)
|
||||
assert.Equal(t, scenario.wantFailures, errs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -18,17 +18,18 @@ package validation
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/resource"
|
||||
"k8s.io/utils/pointer"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func testClaim(name, namespace string, spec resource.ResourceClaimSpec) *resource.ResourceClaim {
|
||||
|
|
@ -37,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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,306 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/kubernetes/pkg/apis/resource"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func testResourceClaimParameters(name, namespace string, requests []resource.DriverRequests) *resource.ResourceClaimParameters {
|
||||
return &resource.ResourceClaimParameters{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
DriverRequests: requests,
|
||||
}
|
||||
}
|
||||
|
||||
var goodRequests []resource.DriverRequests
|
||||
|
||||
func TestValidateResourceClaimParameters(t *testing.T) {
|
||||
goodName := "foo"
|
||||
badName := "!@#$%^"
|
||||
badValue := "spaces not allowed"
|
||||
now := metav1.Now()
|
||||
|
||||
scenarios := map[string]struct {
|
||||
parameters *resource.ResourceClaimParameters
|
||||
wantFailures field.ErrorList
|
||||
}{
|
||||
"good": {
|
||||
parameters: testResourceClaimParameters(goodName, goodName, goodRequests),
|
||||
},
|
||||
"missing-name": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
|
||||
parameters: testResourceClaimParameters("", goodName, goodRequests),
|
||||
},
|
||||
"missing-namespace": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
|
||||
parameters: testResourceClaimParameters(goodName, "", goodRequests),
|
||||
},
|
||||
"bad-name": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
parameters: testResourceClaimParameters(badName, goodName, goodRequests),
|
||||
},
|
||||
"bad-namespace": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')")},
|
||||
parameters: testResourceClaimParameters(goodName, badName, goodRequests),
|
||||
},
|
||||
"generate-name": {
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.GenerateName = "prefix-"
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"uid": {
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"resource-version": {
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.ResourceVersion = "1"
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"generation": {
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.Generation = 100
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"creation-timestamp": {
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.CreationTimestamp = now
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"deletion-grace-period-seconds": {
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.DeletionGracePeriodSeconds = ptr.To[int64](10)
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"owner-references": {
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "pod",
|
||||
Name: "foo",
|
||||
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
|
||||
},
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"finalizers": {
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.Finalizers = []string{
|
||||
"example.com/foo",
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"managed-fields": {
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.ManagedFields = []metav1.ManagedFieldsEntry{
|
||||
{
|
||||
FieldsType: "FieldsV1",
|
||||
Operation: "Apply",
|
||||
APIVersion: "apps/v1",
|
||||
Manager: "foo",
|
||||
},
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"good-labels": {
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.Labels = map[string]string{
|
||||
"apps.kubernetes.io/name": "test",
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"bad-labels": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.Labels = map[string]string{
|
||||
"hello-world": badValue,
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"good-annotations": {
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.Annotations = map[string]string{
|
||||
"foo": "bar",
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"bad-annotations": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.Annotations = map[string]string{
|
||||
badName: "hello world",
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
|
||||
"empty-model": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("driverRequests").Index(0).Child("requests").Index(0), "exactly one structured model field must be set")},
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.DriverRequests = []resource.DriverRequests{{DriverName: goodName, Requests: []resource.ResourceRequest{{}}}}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
|
||||
"empty-requests": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("driverRequests").Index(0).Child("requests"), "empty entries with no requests are not allowed")},
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.DriverRequests = []resource.DriverRequests{{DriverName: goodName}}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
|
||||
"invalid-driver": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverRequests").Index(1).Child("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.DriverRequests = []resource.DriverRequests{
|
||||
{
|
||||
DriverName: goodName,
|
||||
Requests: []resource.ResourceRequest{
|
||||
{
|
||||
ResourceRequestModel: resource.ResourceRequestModel{
|
||||
NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
DriverName: badName,
|
||||
Requests: []resource.ResourceRequest{
|
||||
{
|
||||
ResourceRequestModel: resource.ResourceRequestModel{
|
||||
NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
|
||||
"duplicate-driver": {
|
||||
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("driverRequests").Index(1).Child("driverName"), goodName)},
|
||||
parameters: func() *resource.ResourceClaimParameters {
|
||||
parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
|
||||
parameters.DriverRequests = []resource.DriverRequests{
|
||||
{
|
||||
DriverName: goodName,
|
||||
Requests: []resource.ResourceRequest{
|
||||
{
|
||||
ResourceRequestModel: resource.ResourceRequestModel{
|
||||
NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
DriverName: goodName,
|
||||
Requests: []resource.ResourceRequest{
|
||||
{
|
||||
ResourceRequestModel: resource.ResourceRequestModel{
|
||||
NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for name, scenario := range scenarios {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
errs := ValidateResourceClaimParameters(scenario.parameters)
|
||||
assert.Equal(t, scenario.wantFailures, errs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateResourceClaimParametersUpdate(t *testing.T) {
|
||||
name := "valid"
|
||||
validResourceClaimParameters := testResourceClaimParameters(name, name, nil)
|
||||
|
||||
scenarios := map[string]struct {
|
||||
oldResourceClaimParameters *resource.ResourceClaimParameters
|
||||
update func(claim *resource.ResourceClaimParameters) *resource.ResourceClaimParameters
|
||||
wantFailures field.ErrorList
|
||||
}{
|
||||
"valid-no-op-update": {
|
||||
oldResourceClaimParameters: validResourceClaimParameters,
|
||||
update: func(claim *resource.ResourceClaimParameters) *resource.ResourceClaimParameters { return claim },
|
||||
},
|
||||
"invalid-name-update": {
|
||||
oldResourceClaimParameters: validResourceClaimParameters,
|
||||
update: func(claim *resource.ResourceClaimParameters) *resource.ResourceClaimParameters {
|
||||
claim.Name += "-update"
|
||||
return claim
|
||||
},
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
|
||||
},
|
||||
}
|
||||
|
||||
for name, scenario := range scenarios {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
scenario.oldResourceClaimParameters.ResourceVersion = "1"
|
||||
errs := ValidateResourceClaimParametersUpdate(scenario.update(scenario.oldResourceClaimParameters.DeepCopy()), scenario.oldResourceClaimParameters)
|
||||
assert.Equal(t, scenario.wantFailures, errs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -34,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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,301 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/resource"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func testClass(name, driverName string) *resource.ResourceClass {
|
||||
return &resource.ResourceClass{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
DriverName: driverName,
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClass(t *testing.T) {
|
||||
goodName := "foo"
|
||||
now := metav1.Now()
|
||||
goodParameters := resource.ResourceClassParametersReference{
|
||||
Name: "valid",
|
||||
Namespace: "valid",
|
||||
Kind: "foo",
|
||||
}
|
||||
badName := "!@#$%^"
|
||||
badValue := "spaces not allowed"
|
||||
badAPIGroup := "example.com/v1"
|
||||
goodAPIGroup := "example.com"
|
||||
|
||||
scenarios := map[string]struct {
|
||||
class *resource.ResourceClass
|
||||
wantFailures field.ErrorList
|
||||
}{
|
||||
"good-class": {
|
||||
class: testClass(goodName, goodName),
|
||||
},
|
||||
"good-long-driver-name": {
|
||||
class: testClass(goodName, "acme.example.com"),
|
||||
},
|
||||
"missing-name": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
|
||||
class: testClass("", goodName),
|
||||
},
|
||||
"bad-name": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
class: testClass(badName, goodName),
|
||||
},
|
||||
"generate-name": {
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.GenerateName = "pvc-"
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"uid": {
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"resource-version": {
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.ResourceVersion = "1"
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"generation": {
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.Generation = 100
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"creation-timestamp": {
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.CreationTimestamp = now
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"deletion-grace-period-seconds": {
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.DeletionGracePeriodSeconds = pointer.Int64(10)
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"owner-references": {
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "pod",
|
||||
Name: "foo",
|
||||
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
|
||||
},
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"finalizers": {
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.Finalizers = []string{
|
||||
"example.com/foo",
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"managed-fields": {
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.ManagedFields = []metav1.ManagedFieldsEntry{
|
||||
{
|
||||
FieldsType: "FieldsV1",
|
||||
Operation: "Apply",
|
||||
APIVersion: "apps/v1",
|
||||
Manager: "foo",
|
||||
},
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"good-labels": {
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.Labels = map[string]string{
|
||||
"apps.kubernetes.io/name": "test",
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"bad-labels": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.Labels = map[string]string{
|
||||
"hello-world": badValue,
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"good-annotations": {
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.Annotations = map[string]string{
|
||||
"foo": "bar",
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"bad-annotations": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.Annotations = map[string]string{
|
||||
badName: "hello world",
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"missing-driver-name": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("driverName"), ""),
|
||||
field.Invalid(field.NewPath("driverName"), "", "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||
},
|
||||
class: testClass(goodName, ""),
|
||||
},
|
||||
"invalid-driver-name": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
class: testClass(goodName, badName),
|
||||
},
|
||||
"invalid-qualified-driver-name": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), goodName+"/path", "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
class: testClass(goodName, goodName+"/path"),
|
||||
},
|
||||
"good-parameters": {
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.ParametersRef = goodParameters.DeepCopy()
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"good-parameters-apigroup": {
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.ParametersRef = goodParameters.DeepCopy()
|
||||
class.ParametersRef.APIGroup = goodAPIGroup
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"bad-parameters-apigroup": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("parametersRef", "apiGroup"), badAPIGroup, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.ParametersRef = goodParameters.DeepCopy()
|
||||
class.ParametersRef.APIGroup = badAPIGroup
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"missing-parameters-name": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("parametersRef", "name"), "")},
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.ParametersRef = goodParameters.DeepCopy()
|
||||
class.ParametersRef.Name = ""
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"bad-parameters-namespace": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("parametersRef", "namespace"), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')")},
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.ParametersRef = goodParameters.DeepCopy()
|
||||
class.ParametersRef.Namespace = badName
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"missing-parameters-kind": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("parametersRef", "kind"), "")},
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.ParametersRef = goodParameters.DeepCopy()
|
||||
class.ParametersRef.Kind = ""
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
"invalid-node-selector": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("suitableNodes", "nodeSelectorTerms"), "must have at least one node selector term")},
|
||||
class: func() *resource.ResourceClass {
|
||||
class := testClass(goodName, goodName)
|
||||
class.SuitableNodes = &core.NodeSelector{
|
||||
// Must not be empty.
|
||||
}
|
||||
return class
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for name, scenario := range scenarios {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
errs := ValidateClass(scenario.class)
|
||||
assert.Equal(t, scenario.wantFailures, errs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClassUpdate(t *testing.T) {
|
||||
validClass := testClass("foo", "valid")
|
||||
|
||||
scenarios := map[string]struct {
|
||||
oldClass *resource.ResourceClass
|
||||
update func(class *resource.ResourceClass) *resource.ResourceClass
|
||||
wantFailures field.ErrorList
|
||||
}{
|
||||
"valid-no-op-update": {
|
||||
oldClass: validClass,
|
||||
update: func(class *resource.ResourceClass) *resource.ResourceClass { return class },
|
||||
},
|
||||
"update-driver": {
|
||||
oldClass: validClass,
|
||||
update: func(class *resource.ResourceClass) *resource.ResourceClass {
|
||||
class.DriverName += "2"
|
||||
return class
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, scenario := range scenarios {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
scenario.oldClass.ResourceVersion = "1"
|
||||
errs := ValidateClassUpdate(scenario.update(scenario.oldClass.DeepCopy()), scenario.oldClass)
|
||||
assert.Equal(t, scenario.wantFailures, errs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,313 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/kubernetes/pkg/apis/resource"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func testResourceClassParameters(name, namespace string, filters []resource.ResourceFilter) *resource.ResourceClassParameters {
|
||||
return &resource.ResourceClassParameters{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Filters: filters,
|
||||
}
|
||||
}
|
||||
|
||||
var goodFilters []resource.ResourceFilter
|
||||
|
||||
func TestValidateResourceClassParameters(t *testing.T) {
|
||||
goodName := "foo"
|
||||
badName := "!@#$%^"
|
||||
badValue := "spaces not allowed"
|
||||
now := metav1.Now()
|
||||
|
||||
scenarios := map[string]struct {
|
||||
parameters *resource.ResourceClassParameters
|
||||
wantFailures field.ErrorList
|
||||
}{
|
||||
"good": {
|
||||
parameters: testResourceClassParameters(goodName, goodName, goodFilters),
|
||||
},
|
||||
"missing-name": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
|
||||
parameters: testResourceClassParameters("", goodName, goodFilters),
|
||||
},
|
||||
"missing-namespace": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
|
||||
parameters: testResourceClassParameters(goodName, "", goodFilters),
|
||||
},
|
||||
"bad-name": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
parameters: testResourceClassParameters(badName, goodName, goodFilters),
|
||||
},
|
||||
"bad-namespace": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')")},
|
||||
parameters: testResourceClassParameters(goodName, badName, goodFilters),
|
||||
},
|
||||
"generate-name": {
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.GenerateName = "prefix-"
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"uid": {
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"resource-version": {
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.ResourceVersion = "1"
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"generation": {
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.Generation = 100
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"creation-timestamp": {
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.CreationTimestamp = now
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"deletion-grace-period-seconds": {
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.DeletionGracePeriodSeconds = ptr.To[int64](10)
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"owner-references": {
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "pod",
|
||||
Name: "foo",
|
||||
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
|
||||
},
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"finalizers": {
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.Finalizers = []string{
|
||||
"example.com/foo",
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"managed-fields": {
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.ManagedFields = []metav1.ManagedFieldsEntry{
|
||||
{
|
||||
FieldsType: "FieldsV1",
|
||||
Operation: "Apply",
|
||||
APIVersion: "apps/v1",
|
||||
Manager: "foo",
|
||||
},
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"good-labels": {
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.Labels = map[string]string{
|
||||
"apps.kubernetes.io/name": "test",
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"bad-labels": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.Labels = map[string]string{
|
||||
"hello-world": badValue,
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"good-annotations": {
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.Annotations = map[string]string{
|
||||
"foo": "bar",
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
"bad-annotations": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.Annotations = map[string]string{
|
||||
badName: "hello world",
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
|
||||
"empty-model": {
|
||||
wantFailures: field.ErrorList{field.Required(field.NewPath("filters").Index(0), "exactly one structured model field must be set")},
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.Filters = []resource.ResourceFilter{{DriverName: goodName}}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
|
||||
"filters-invalid-driver": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("filters").Index(1).Child("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.Filters = []resource.ResourceFilter{
|
||||
{
|
||||
DriverName: goodName,
|
||||
ResourceFilterModel: resource.ResourceFilterModel{
|
||||
NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
|
||||
},
|
||||
},
|
||||
{
|
||||
DriverName: badName,
|
||||
ResourceFilterModel: resource.ResourceFilterModel{
|
||||
NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
|
||||
},
|
||||
},
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
|
||||
"filters-duplicate-driver": {
|
||||
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("filters").Index(1).Child("driverName"), goodName)},
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.Filters = []resource.ResourceFilter{
|
||||
{
|
||||
DriverName: goodName,
|
||||
ResourceFilterModel: resource.ResourceFilterModel{
|
||||
NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
|
||||
},
|
||||
},
|
||||
{
|
||||
DriverName: goodName,
|
||||
ResourceFilterModel: resource.ResourceFilterModel{
|
||||
NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
|
||||
},
|
||||
},
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
|
||||
"parameters-invalid-driver": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("vendorParameters").Index(1).Child("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.VendorParameters = []resource.VendorParameters{
|
||||
{
|
||||
DriverName: goodName,
|
||||
},
|
||||
{
|
||||
DriverName: badName,
|
||||
},
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
|
||||
"parameters-duplicate-driver": {
|
||||
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("vendorParameters").Index(1).Child("driverName"), goodName)},
|
||||
parameters: func() *resource.ResourceClassParameters {
|
||||
parameters := testResourceClassParameters(goodName, goodName, goodFilters)
|
||||
parameters.VendorParameters = []resource.VendorParameters{
|
||||
{
|
||||
DriverName: goodName,
|
||||
},
|
||||
{
|
||||
DriverName: goodName,
|
||||
},
|
||||
}
|
||||
return parameters
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for name, scenario := range scenarios {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
errs := ValidateResourceClassParameters(scenario.parameters)
|
||||
assert.Equal(t, scenario.wantFailures, errs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateResourceClassParametersUpdate(t *testing.T) {
|
||||
name := "valid"
|
||||
validResourceClassParameters := testResourceClassParameters(name, name, nil)
|
||||
|
||||
scenarios := map[string]struct {
|
||||
oldResourceClassParameters *resource.ResourceClassParameters
|
||||
update func(class *resource.ResourceClassParameters) *resource.ResourceClassParameters
|
||||
wantFailures field.ErrorList
|
||||
}{
|
||||
"valid-no-op-update": {
|
||||
oldResourceClassParameters: validResourceClassParameters,
|
||||
update: func(class *resource.ResourceClassParameters) *resource.ResourceClassParameters { return class },
|
||||
},
|
||||
"invalid-name-update": {
|
||||
oldResourceClassParameters: validResourceClassParameters,
|
||||
update: func(class *resource.ResourceClassParameters) *resource.ResourceClassParameters {
|
||||
class.Name += "-update"
|
||||
return class
|
||||
},
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
|
||||
},
|
||||
}
|
||||
|
||||
for name, scenario := range scenarios {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
scenario.oldResourceClassParameters.ResourceVersion = "1"
|
||||
errs := ValidateResourceClassParametersUpdate(scenario.update(scenario.oldResourceClassParameters.DeepCopy()), scenario.oldResourceClassParameters)
|
||||
assert.Equal(t, scenario.wantFailures, errs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -32,10 +32,13 @@ func testResourceSlice(name, nodeName, driverName string) *resource.ResourceSlic
|
|||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
NodeName: nodeName,
|
||||
DriverName: driverName,
|
||||
ResourceModel: resource.ResourceModel{
|
||||
NamedResources: &resource.NamedResourcesResources{},
|
||||
Spec: resource.ResourceSliceSpec{
|
||||
NodeName: nodeName,
|
||||
Driver: driverName,
|
||||
Pool: resource.ResourcePool{
|
||||
Name: nodeName,
|
||||
ResourceSliceCount: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -180,22 +183,24 @@ func TestValidateResourceSlice(t *testing.T) {
|
|||
}(),
|
||||
},
|
||||
"bad-nodename": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("nodeName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
slice: testResourceSlice(goodName, badName, driverName),
|
||||
wantFailures: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "pool", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||
field.Invalid(field.NewPath("spec", "nodeName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||
},
|
||||
slice: testResourceSlice(goodName, badName, driverName),
|
||||
},
|
||||
"bad-multi-pool-name": {
|
||||
wantFailures: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "pool", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||
field.Invalid(field.NewPath("spec", "pool", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||
field.Invalid(field.NewPath("spec", "nodeName"), badName+"/"+badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||
},
|
||||
slice: testResourceSlice(goodName, badName+"/"+badName, driverName),
|
||||
},
|
||||
"bad-drivername": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "driver"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||
slice: testResourceSlice(goodName, goodName, badName),
|
||||
},
|
||||
|
||||
"empty-model": {
|
||||
wantFailures: field.ErrorList{field.Required(nil, "exactly one structured model field must be set")},
|
||||
slice: func() *resource.ResourceSlice {
|
||||
slice := testResourceSlice(goodName, goodName, driverName)
|
||||
slice.ResourceModel = resource.ResourceModel{}
|
||||
return slice
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for name, scenario := range scenarios {
|
||||
|
|
@ -228,18 +233,26 @@ func TestValidateResourceSliceUpdate(t *testing.T) {
|
|||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
|
||||
},
|
||||
"invalid-update-nodename": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("nodeName"), name+"-updated", "field is immutable")},
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "nodeName"), name+"-updated", "field is immutable")},
|
||||
oldResourceSlice: validResourceSlice,
|
||||
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
|
||||
slice.NodeName += "-updated"
|
||||
slice.Spec.NodeName += "-updated"
|
||||
return slice
|
||||
},
|
||||
},
|
||||
"invalid-update-drivername": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), name+"-updated", "field is immutable")},
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "driver"), name+"-updated", "field is immutable")},
|
||||
oldResourceSlice: validResourceSlice,
|
||||
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
|
||||
slice.DriverName += "-updated"
|
||||
slice.Spec.Driver += "-updated"
|
||||
return slice
|
||||
},
|
||||
},
|
||||
"invalid-update-pool": {
|
||||
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "pool", "name"), validResourceSlice.Spec.Pool.Name+"-updated", "field is immutable")},
|
||||
oldResourceSlice: validResourceSlice,
|
||||
update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
|
||||
slice.Spec.Pool.Name += "-updated"
|
||||
return slice
|
||||
},
|
||||
},
|
||||
|
|
|
|||
943
pkg/apis/resource/zz_generated.deepcopy.go
generated
943
pkg/apis/resource/zz_generated.deepcopy.go
generated
File diff suppressed because it is too large
Load diff
1957
pkg/generated/openapi/zz_generated.openapi.go
generated
1957
pkg/generated/openapi/zz_generated.openapi.go
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)},
|
||||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
85
pkg/registry/resource/deviceclass/strategy.go
Normal file
85
pkg/registry/resource/deviceclass/strategy.go
Normal 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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -29,9 +29,6 @@ var resourceClaim = &resource.ResourceClaim{
|
|||
Name: "valid-claim",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: resource.ResourceClaimSpec{
|
||||
ResourceClassName: "valid-class",
|
||||
},
|
||||
}
|
||||
|
||||
func TestClaimStrategy(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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{}) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
2031
staging/src/k8s.io/api/core/v1/generated.pb.go
generated
2031
staging/src/k8s.io/api/core/v1/generated.pb.go
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
9405
staging/src/k8s.io/api/resource/v1alpha3/generated.pb.go
generated
9405
staging/src/k8s.io/api/resource/v1alpha3/generated.pb.go
generated
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
Loading…
Reference in a new issue