DRA API: add maximum length of opaque parameters

This had been left out unintentionally earlier. Because theoretically there
might now be existing objects with parameters that are larger than whatever
limit gets enforced now, the limit only gets checked when parameters get
created or modified.

This is similar to the validation of CEL expressions and for consistency, the
same 10 Ki limit as for those is chosen.

Because the limit is not enforced for stored parameters, it can be increased in
the future, with the caveat that users who need larger parameters then depend
on the newer Kubernetes release with a higher limit. Lowering the limit is
harder because creating deployments that worked in older Kubernetes will not
work anymore with newer Kubernetes.
This commit is contained in:
Patrick Ohly 2024-11-06 09:42:47 +01:00
parent e273349f3a
commit 446f20aa3e
14 changed files with 209 additions and 26 deletions

View File

@ -15852,7 +15852,7 @@
}, },
"parameters": { "parameters": {
"$ref": "#/definitions/io.k8s.apimachinery.pkg.runtime.RawExtension", "$ref": "#/definitions/io.k8s.apimachinery.pkg.runtime.RawExtension",
"description": "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions." "description": "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions.\n\nThe length of the raw data must be smaller or equal to 10 Ki."
} }
}, },
"required": [ "required": [
@ -16608,7 +16608,7 @@
}, },
"parameters": { "parameters": {
"$ref": "#/definitions/io.k8s.apimachinery.pkg.runtime.RawExtension", "$ref": "#/definitions/io.k8s.apimachinery.pkg.runtime.RawExtension",
"description": "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions." "description": "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions.\n\nThe length of the raw data must be smaller or equal to 10 Ki."
} }
}, },
"required": [ "required": [

View File

@ -586,7 +586,7 @@
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension" "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension"
} }
], ],
"description": "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions." "description": "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions.\n\nThe length of the raw data must be smaller or equal to 10 Ki."
} }
}, },
"required": [ "required": [

View File

@ -608,7 +608,7 @@
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension" "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension"
} }
], ],
"description": "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions." "description": "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions.\n\nThe length of the raw data must be smaller or equal to 10 Ki."
} }
}, },
"required": [ "required": [

View File

@ -654,10 +654,16 @@ type OpaqueDeviceConfiguration struct {
// includes self-identification and a version ("kind" + "apiVersion" for // includes self-identification and a version ("kind" + "apiVersion" for
// Kubernetes types), with conversion between different versions. // Kubernetes types), with conversion between different versions.
// //
// The length of the raw data must be smaller or equal to 10 Ki.
//
// +required // +required
Parameters runtime.RawExtension Parameters runtime.RawExtension
} }
// OpaqueParametersMaxLength is the maximum length of the raw data in an
// [OpaqueDeviceConfiguration.Parameters] field.
const OpaqueParametersMaxLength = 10 * 1024
// ResourceClaimStatus tracks whether the resource has been allocated and what // ResourceClaimStatus tracks whether the resource has been allocated and what
// the result of that was. // the result of that was.
type ResourceClaimStatus struct { type ResourceClaimStatus struct {

View File

@ -110,7 +110,7 @@ func validateDeviceClaim(deviceClaim *resource.DeviceClaim, fldPath *field.Path,
}, fldPath.Child("constraints"))...) }, fldPath.Child("constraints"))...)
allErrs = append(allErrs, validateSlice(deviceClaim.Config, resource.DeviceConfigMaxSize, allErrs = append(allErrs, validateSlice(deviceClaim.Config, resource.DeviceConfigMaxSize,
func(config resource.DeviceClaimConfiguration, fldPath *field.Path) field.ErrorList { func(config resource.DeviceClaimConfiguration, fldPath *field.Path) field.ErrorList {
return validateDeviceClaimConfiguration(config, fldPath, requestNames) return validateDeviceClaimConfiguration(config, fldPath, requestNames, stored)
}, fldPath.Child("config"))...) }, fldPath.Child("config"))...)
return allErrs return allErrs
} }
@ -212,13 +212,13 @@ func validateDeviceConstraint(constraint resource.DeviceConstraint, fldPath *fie
return allErrs return allErrs
} }
func validateDeviceClaimConfiguration(config resource.DeviceClaimConfiguration, fldPath *field.Path, requestNames sets.Set[string]) field.ErrorList { func validateDeviceClaimConfiguration(config resource.DeviceClaimConfiguration, fldPath *field.Path, requestNames sets.Set[string], stored bool) field.ErrorList {
var allErrs field.ErrorList var allErrs field.ErrorList
allErrs = append(allErrs, validateSet(config.Requests, resource.DeviceRequestsMaxSize, allErrs = append(allErrs, validateSet(config.Requests, resource.DeviceRequestsMaxSize,
func(name string, fldPath *field.Path) field.ErrorList { func(name string, fldPath *field.Path) field.ErrorList {
return validateRequestNameRef(name, fldPath, requestNames) return validateRequestNameRef(name, fldPath, requestNames)
}, stringKey, fldPath.Child("requests"))...) }, stringKey, fldPath.Child("requests"))...)
allErrs = append(allErrs, validateDeviceConfiguration(config.DeviceConfiguration, fldPath)...) allErrs = append(allErrs, validateDeviceConfiguration(config.DeviceConfiguration, fldPath, stored)...)
return allErrs return allErrs
} }
@ -230,23 +230,29 @@ func validateRequestNameRef(name string, fldPath *field.Path, requestNames sets.
return allErrs return allErrs
} }
func validateDeviceConfiguration(config resource.DeviceConfiguration, fldPath *field.Path) field.ErrorList { func validateDeviceConfiguration(config resource.DeviceConfiguration, fldPath *field.Path, stored bool) field.ErrorList {
var allErrs field.ErrorList var allErrs field.ErrorList
if config.Opaque == nil { if config.Opaque == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("opaque"), "")) allErrs = append(allErrs, field.Required(fldPath.Child("opaque"), ""))
} else { } else {
allErrs = append(allErrs, validateOpaqueConfiguration(*config.Opaque, fldPath.Child("opaque"))...) allErrs = append(allErrs, validateOpaqueConfiguration(*config.Opaque, fldPath.Child("opaque"), stored)...)
} }
return allErrs return allErrs
} }
func validateOpaqueConfiguration(config resource.OpaqueDeviceConfiguration, fldPath *field.Path) field.ErrorList { func validateOpaqueConfiguration(config resource.OpaqueDeviceConfiguration, fldPath *field.Path, stored bool) field.ErrorList {
var allErrs field.ErrorList var allErrs field.ErrorList
allErrs = append(allErrs, validateDriverName(config.Driver, fldPath.Child("driver"))...) allErrs = append(allErrs, validateDriverName(config.Driver, fldPath.Child("driver"))...)
// Validation of RawExtension as in https://github.com/kubernetes/kubernetes/pull/125549/ // Validation of RawExtension as in https://github.com/kubernetes/kubernetes/pull/125549/
var v any var v any
if len(config.Parameters.Raw) == 0 { if len(config.Parameters.Raw) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("parameters"), "")) allErrs = append(allErrs, field.Required(fldPath.Child("parameters"), ""))
} else if !stored && len(config.Parameters.Raw) > resource.OpaqueParametersMaxLength {
// Don't even bother with parsing when too large.
// Only applies on create. Existing parameters are grand-fathered in
// because the limit was introduced in 1.32. This also means that it
// can be changed in the future.
allErrs = append(allErrs, field.TooLong(fldPath.Child("parameters"), "" /* unused */, resource.OpaqueParametersMaxLength))
} else if err := json.Unmarshal(config.Parameters.Raw, &v); err != nil { } else if err := json.Unmarshal(config.Parameters.Raw, &v); err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("parameters"), "<value omitted>", fmt.Sprintf("error parsing data as JSON: %v", err.Error()))) allErrs = append(allErrs, field.Invalid(fldPath.Child("parameters"), "<value omitted>", fmt.Sprintf("error parsing data as JSON: %v", err.Error())))
} else if v == nil { } else if v == nil {
@ -290,7 +296,7 @@ func validateResourceClaimStatusUpdate(status, oldStatus *resource.ResourceClaim
if oldStatus.Allocation != nil && status.Allocation != nil { if oldStatus.Allocation != nil && status.Allocation != nil {
allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(status.Allocation, oldStatus.Allocation, fldPath.Child("allocation"))...) allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(status.Allocation, oldStatus.Allocation, fldPath.Child("allocation"))...)
} else if status.Allocation != nil { } else if status.Allocation != nil {
allErrs = append(allErrs, validateAllocationResult(status.Allocation, fldPath.Child("allocation"), requestNames)...) allErrs = append(allErrs, validateAllocationResult(status.Allocation, fldPath.Child("allocation"), requestNames, false)...)
} }
return allErrs return allErrs
@ -313,16 +319,16 @@ func validateResourceClaimUserReference(ref resource.ResourceClaimConsumerRefere
// validateAllocationResult enforces constraints for *new* results, which in at // validateAllocationResult enforces constraints for *new* results, which in at
// least one case (admin access) are more strict than before. Therefore it // least one case (admin access) are more strict than before. Therefore it
// may not be called to re-validate results which were stored earlier. // may not be called to re-validate results which were stored earlier.
func validateAllocationResult(allocation *resource.AllocationResult, fldPath *field.Path, requestNames sets.Set[string]) field.ErrorList { func validateAllocationResult(allocation *resource.AllocationResult, fldPath *field.Path, requestNames sets.Set[string], stored bool) field.ErrorList {
var allErrs field.ErrorList var allErrs field.ErrorList
allErrs = append(allErrs, validateDeviceAllocationResult(allocation.Devices, fldPath.Child("devices"), requestNames)...) allErrs = append(allErrs, validateDeviceAllocationResult(allocation.Devices, fldPath.Child("devices"), requestNames, stored)...)
if allocation.NodeSelector != nil { if allocation.NodeSelector != nil {
allErrs = append(allErrs, corevalidation.ValidateNodeSelector(allocation.NodeSelector, fldPath.Child("nodeSelector"))...) allErrs = append(allErrs, corevalidation.ValidateNodeSelector(allocation.NodeSelector, fldPath.Child("nodeSelector"))...)
} }
return allErrs return allErrs
} }
func validateDeviceAllocationResult(allocation resource.DeviceAllocationResult, fldPath *field.Path, requestNames sets.Set[string]) field.ErrorList { func validateDeviceAllocationResult(allocation resource.DeviceAllocationResult, fldPath *field.Path, requestNames sets.Set[string], stored bool) field.ErrorList {
var allErrs field.ErrorList var allErrs field.ErrorList
allErrs = append(allErrs, validateSlice(allocation.Results, resource.AllocationResultsMaxSize, allErrs = append(allErrs, validateSlice(allocation.Results, resource.AllocationResultsMaxSize,
func(result resource.DeviceRequestAllocationResult, fldPath *field.Path) field.ErrorList { func(result resource.DeviceRequestAllocationResult, fldPath *field.Path) field.ErrorList {
@ -330,7 +336,7 @@ func validateDeviceAllocationResult(allocation resource.DeviceAllocationResult,
}, fldPath.Child("results"))...) }, fldPath.Child("results"))...)
allErrs = append(allErrs, validateSlice(allocation.Config, 2*resource.DeviceConfigMaxSize, /* class + claim */ allErrs = append(allErrs, validateSlice(allocation.Config, 2*resource.DeviceConfigMaxSize, /* class + claim */
func(config resource.DeviceAllocationConfiguration, fldPath *field.Path) field.ErrorList { func(config resource.DeviceAllocationConfiguration, fldPath *field.Path) field.ErrorList {
return validateDeviceAllocationConfiguration(config, fldPath, requestNames) return validateDeviceAllocationConfiguration(config, fldPath, requestNames, stored)
}, fldPath.Child("config"))...) }, fldPath.Child("config"))...)
return allErrs return allErrs
@ -345,14 +351,14 @@ func validateDeviceRequestAllocationResult(result resource.DeviceRequestAllocati
return allErrs return allErrs
} }
func validateDeviceAllocationConfiguration(config resource.DeviceAllocationConfiguration, fldPath *field.Path, requestNames sets.Set[string]) field.ErrorList { func validateDeviceAllocationConfiguration(config resource.DeviceAllocationConfiguration, fldPath *field.Path, requestNames sets.Set[string], stored bool) field.ErrorList {
var allErrs field.ErrorList var allErrs field.ErrorList
allErrs = append(allErrs, validateAllocationConfigSource(config.Source, fldPath.Child("source"))...) allErrs = append(allErrs, validateAllocationConfigSource(config.Source, fldPath.Child("source"))...)
allErrs = append(allErrs, validateSet(config.Requests, resource.DeviceRequestsMaxSize, allErrs = append(allErrs, validateSet(config.Requests, resource.DeviceRequestsMaxSize,
func(name string, fldPath *field.Path) field.ErrorList { func(name string, fldPath *field.Path) field.ErrorList {
return validateRequestNameRef(name, fldPath, requestNames) return validateRequestNameRef(name, fldPath, requestNames)
}, stringKey, fldPath.Child("requests"))...) }, stringKey, fldPath.Child("requests"))...)
allErrs = append(allErrs, validateDeviceConfiguration(config.DeviceConfiguration, fldPath)...) allErrs = append(allErrs, validateDeviceConfiguration(config.DeviceConfiguration, fldPath, stored)...)
return allErrs return allErrs
} }
@ -396,12 +402,20 @@ func validateDeviceClassSpec(spec, oldSpec *resource.DeviceClassSpec, fldPath *f
return validateSelector(selector, fldPath, stored) return validateSelector(selector, fldPath, stored)
}, },
fldPath.Child("selectors"))...) fldPath.Child("selectors"))...)
allErrs = append(allErrs, validateSlice(spec.Config, resource.DeviceConfigMaxSize, validateDeviceClassConfiguration, fldPath.Child("config"))...) // Same logic as above for configs.
if oldSpec != nil {
stored = apiequality.Semantic.DeepEqual(spec.Config, oldSpec.Config)
}
allErrs = append(allErrs, validateSlice(spec.Config, resource.DeviceConfigMaxSize,
func(config resource.DeviceClassConfiguration, fldPath *field.Path) field.ErrorList {
return validateDeviceClassConfiguration(config, fldPath, stored)
},
fldPath.Child("config"))...)
return allErrs return allErrs
} }
func validateDeviceClassConfiguration(config resource.DeviceClassConfiguration, fldPath *field.Path) field.ErrorList { func validateDeviceClassConfiguration(config resource.DeviceClassConfiguration, fldPath *field.Path, stored bool) field.ErrorList {
return validateDeviceConfiguration(config.DeviceConfiguration, fldPath) return validateDeviceConfiguration(config.DeviceConfiguration, fldPath, stored)
} }
// ValidateResourceClaimTemplate validates a ResourceClaimTemplate. // ValidateResourceClaimTemplate validates a ResourceClaimTemplate.

View File

@ -17,6 +17,7 @@ limitations under the License.
package validation package validation
import ( import (
"strings"
"testing" "testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -227,6 +228,7 @@ func TestValidateClass(t *testing.T) {
field.Invalid(field.NewPath("spec", "config").Index(3).Child("opaque", "parameters"), "<value omitted>", "parameters must be a valid JSON object"), field.Invalid(field.NewPath("spec", "config").Index(3).Child("opaque", "parameters"), "<value omitted>", "parameters must be a valid JSON object"),
field.Required(field.NewPath("spec", "config").Index(4).Child("opaque", "parameters"), ""), field.Required(field.NewPath("spec", "config").Index(4).Child("opaque", "parameters"), ""),
field.Required(field.NewPath("spec", "config").Index(5).Child("opaque"), ""), field.Required(field.NewPath("spec", "config").Index(5).Child("opaque"), ""),
field.TooLong(field.NewPath("spec", "config").Index(7).Child("opaque", "parameters"), "" /* unused */, resource.OpaqueParametersMaxLength),
}, },
class: func() *resource.DeviceClass { class: func() *resource.DeviceClass {
class := testClass(goodName) class := testClass(goodName)
@ -272,6 +274,22 @@ func TestValidateClass(t *testing.T) {
{ {
DeviceConfiguration: resource.DeviceConfiguration{ /* Bad, empty. */ }, DeviceConfiguration: resource.DeviceConfiguration{ /* Bad, empty. */ },
}, },
{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: goodName,
Parameters: runtime.RawExtension{Raw: []byte(`{"str": "` + strings.Repeat("x", resource.OpaqueParametersMaxLength-9-2) + `"}`)},
},
},
},
{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: goodName,
Parameters: runtime.RawExtension{Raw: []byte(`{"str": "` + strings.Repeat("x", resource.OpaqueParametersMaxLength-9-2+1 /* too large by one */) + `"}`)},
},
},
},
} }
for i := len(class.Spec.Config); i < resource.DeviceConfigMaxSize; i++ { for i := len(class.Spec.Config); i < resource.DeviceConfigMaxSize; i++ {
class.Spec.Config = append(class.Spec.Config, validConfig) class.Spec.Config = append(class.Spec.Config, validConfig)
@ -321,6 +339,55 @@ func TestValidateClassUpdate(t *testing.T) {
oldClass: validClass, oldClass: validClass,
update: func(class *resource.DeviceClass) *resource.DeviceClass { return class }, update: func(class *resource.DeviceClass) *resource.DeviceClass { return class },
}, },
"valid-config-large": {
oldClass: validClass,
update: func(class *resource.DeviceClass) *resource.DeviceClass {
class.Spec.Config = []resource.DeviceClassConfiguration{{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: goodName,
Parameters: runtime.RawExtension{Raw: []byte(`{"str": "` + strings.Repeat("x", resource.OpaqueParametersMaxLength-9-2) + `"}`)},
},
},
}}
return class
},
},
"invalid-config-too-large": {
wantFailures: field.ErrorList{
field.TooLong(field.NewPath("spec", "config").Index(0).Child("opaque", "parameters"), "" /* unused */, resource.OpaqueParametersMaxLength),
},
oldClass: validClass,
update: func(class *resource.DeviceClass) *resource.DeviceClass {
class.Spec.Config = []resource.DeviceClassConfiguration{{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: goodName,
Parameters: runtime.RawExtension{Raw: []byte(`{"str": "` + strings.Repeat("x", resource.OpaqueParametersMaxLength-9-2+1 /* too large by one */) + `"}`)},
},
},
}}
return class
},
},
"too-large-config-valid-if-stored": {
oldClass: func() *resource.DeviceClass {
class := validClass.DeepCopy()
class.Spec.Config = []resource.DeviceClassConfiguration{{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: goodName,
Parameters: runtime.RawExtension{Raw: []byte(`{"str": "` + strings.Repeat("x", resource.OpaqueParametersMaxLength-9-2+1 /* too large by one */) + `"}`)},
},
},
}}
return class
}(),
update: func(class *resource.DeviceClass) *resource.DeviceClass {
// No changes -> remains valid.
return class
},
},
} }
for name, scenario := range scenarios { for name, scenario := range scenarios {

View File

@ -393,12 +393,13 @@ func TestValidateClaim(t *testing.T) {
return claim return claim
}(), }(),
}, },
"invalid-config-json": { "configuration": {
wantFailures: field.ErrorList{ wantFailures: field.ErrorList{
field.Required(field.NewPath("spec", "devices", "config").Index(0).Child("opaque", "parameters"), ""), field.Required(field.NewPath("spec", "devices", "config").Index(0).Child("opaque", "parameters"), ""),
field.Invalid(field.NewPath("spec", "devices", "config").Index(1).Child("opaque", "parameters"), "<value omitted>", "error parsing data as JSON: unexpected end of JSON input"), field.Invalid(field.NewPath("spec", "devices", "config").Index(1).Child("opaque", "parameters"), "<value omitted>", "error parsing data as JSON: unexpected end of JSON input"),
field.Invalid(field.NewPath("spec", "devices", "config").Index(2).Child("opaque", "parameters"), "<value omitted>", "parameters must be a valid JSON object"), field.Invalid(field.NewPath("spec", "devices", "config").Index(2).Child("opaque", "parameters"), "<value omitted>", "parameters must be a valid JSON object"),
field.Required(field.NewPath("spec", "devices", "config").Index(3).Child("opaque", "parameters"), ""), field.Required(field.NewPath("spec", "devices", "config").Index(3).Child("opaque", "parameters"), ""),
field.TooLong(field.NewPath("spec", "devices", "config").Index(5).Child("opaque", "parameters"), "" /* unused */, resource.OpaqueParametersMaxLength),
}, },
claim: func() *resource.ResourceClaim { claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, validClaimSpec) claim := testClaim(goodName, goodNS, validClaimSpec)
@ -443,6 +444,26 @@ func TestValidateClaim(t *testing.T) {
}, },
}, },
}, },
{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: "dra.example.com",
Parameters: runtime.RawExtension{
Raw: []byte(`{"str": "` + strings.Repeat("x", resource.OpaqueParametersMaxLength-9-2) + `"}`),
},
},
},
},
{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: "dra.example.com",
Parameters: runtime.RawExtension{
Raw: []byte(`{"str": "` + strings.Repeat("x", resource.OpaqueParametersMaxLength-9-2+1 /* too large by one */) + `"}`),
},
},
},
},
} }
return claim return claim
}(), }(),
@ -534,7 +555,7 @@ func TestValidateClaimUpdate(t *testing.T) {
oldClaim: validClaim, oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { return claim }, update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { return claim },
}, },
"invalid-update-class": { "invalid-update": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimSpec { wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimSpec {
spec := validClaim.Spec.DeepCopy() spec := validClaim.Spec.DeepCopy()
spec.Devices.Requests[0].DeviceClassName += "2" spec.Devices.Requests[0].DeviceClassName += "2"
@ -546,6 +567,24 @@ func TestValidateClaimUpdate(t *testing.T) {
return claim return claim
}, },
}, },
"too-large-config-valid-if-stored": {
oldClaim: func() *resource.ResourceClaim {
claim := validClaim.DeepCopy()
claim.Spec.Devices.Config = []resource.DeviceClaimConfiguration{{
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: goodName,
Parameters: runtime.RawExtension{Raw: []byte(`{"str": "` + strings.Repeat("x", resource.OpaqueParametersMaxLength-9-2+1 /* too large by one */) + `"}`)},
},
},
}}
return claim
}(),
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
// No changes -> remains valid.
return claim
},
},
} }
for name, scenario := range scenarios { for name, scenario := range scenarios {
@ -849,6 +888,7 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
field.Required(field.NewPath("status", "allocation", "devices", "config").Index(4).Child("opaque", "driver"), ""), field.Required(field.NewPath("status", "allocation", "devices", "config").Index(4).Child("opaque", "driver"), ""),
field.Invalid(field.NewPath("status", "allocation", "devices", "config").Index(4).Child("opaque", "driver"), "", "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"), field.Invalid(field.NewPath("status", "allocation", "devices", "config").Index(4).Child("opaque", "driver"), "", "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
field.Required(field.NewPath("status", "allocation", "devices", "config").Index(4).Child("opaque", "parameters"), ""), field.Required(field.NewPath("status", "allocation", "devices", "config").Index(4).Child("opaque", "parameters"), ""),
field.TooLong(field.NewPath("status", "allocation", "devices", "config").Index(6).Child("opaque", "parameters"), "" /* unused */, resource.OpaqueParametersMaxLength),
}, },
oldClaim: validClaim, oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
@ -898,11 +938,51 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
Opaque: &resource.OpaqueDeviceConfiguration{ /* Empty! */ }, Opaque: &resource.OpaqueDeviceConfiguration{ /* Empty! */ },
}, },
}, },
{
Source: resource.AllocationConfigSourceClaim,
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: goodName,
Parameters: runtime.RawExtension{Raw: []byte(`{"str": "` + strings.Repeat("x", resource.OpaqueParametersMaxLength-9-2) + `"}`)},
},
},
},
{
Source: resource.AllocationConfigSourceClaim,
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: goodName,
Parameters: runtime.RawExtension{Raw: []byte(`{"str": "` + strings.Repeat("x", resource.OpaqueParametersMaxLength-9-2+1 /* too large by one */) + `"}`)},
},
},
},
// Other invalid resource.DeviceConfiguration are covered elsewhere. */ // Other invalid resource.DeviceConfiguration are covered elsewhere. */
} }
return claim return claim
}, },
}, },
"valid-configuration-update": {
oldClaim: func() *resource.ResourceClaim {
claim := validClaim.DeepCopy()
claim.Status.Allocation = validAllocatedClaim.Status.Allocation.DeepCopy()
claim.Status.Allocation.Devices.Config = []resource.DeviceAllocationConfiguration{
{
Source: resource.AllocationConfigSourceClaim,
DeviceConfiguration: resource.DeviceConfiguration{
Opaque: &resource.OpaqueDeviceConfiguration{
Driver: goodName,
Parameters: runtime.RawExtension{Raw: []byte(`{"str": "` + strings.Repeat("x", resource.OpaqueParametersMaxLength-9-2+1 /* too large by one */) + `"}`)},
},
},
},
}
return claim
}(),
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
// No change -> remains valid.
return claim
},
},
} }
for name, scenario := range scenarios { for name, scenario := range scenarios {

View File

@ -47061,7 +47061,7 @@ func schema_k8sio_api_resource_v1alpha3_OpaqueDeviceConfiguration(ref common.Ref
}, },
"parameters": { "parameters": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Description: "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions.", Description: "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions.\n\nThe length of the raw data must be smaller or equal to 10 Ki.",
Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"),
}, },
}, },
@ -48413,7 +48413,7 @@ func schema_k8sio_api_resource_v1beta1_OpaqueDeviceConfiguration(ref common.Refe
}, },
"parameters": { "parameters": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Description: "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions.", Description: "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions.\n\nThe length of the raw data must be smaller or equal to 10 Ki.",
Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"),
}, },
}, },

View File

@ -500,6 +500,8 @@ message OpaqueDeviceConfiguration {
// includes self-identification and a version ("kind" + "apiVersion" for // includes self-identification and a version ("kind" + "apiVersion" for
// Kubernetes types), with conversion between different versions. // Kubernetes types), with conversion between different versions.
// //
// The length of the raw data must be smaller or equal to 10 Ki.
//
// +required // +required
optional .k8s.io.apimachinery.pkg.runtime.RawExtension parameters = 2; optional .k8s.io.apimachinery.pkg.runtime.RawExtension parameters = 2;
} }

View File

@ -652,10 +652,16 @@ type OpaqueDeviceConfiguration struct {
// includes self-identification and a version ("kind" + "apiVersion" for // includes self-identification and a version ("kind" + "apiVersion" for
// Kubernetes types), with conversion between different versions. // Kubernetes types), with conversion between different versions.
// //
// The length of the raw data must be smaller or equal to 10 Ki.
//
// +required // +required
Parameters runtime.RawExtension `json:"parameters" protobuf:"bytes,2,name=parameters"` Parameters runtime.RawExtension `json:"parameters" protobuf:"bytes,2,name=parameters"`
} }
// OpaqueParametersMaxLength is the maximum length of the raw data in an
// [OpaqueDeviceConfiguration.Parameters] field.
const OpaqueParametersMaxLength = 10 * 1024
// ResourceClaimStatus tracks whether the resource has been allocated and what // ResourceClaimStatus tracks whether the resource has been allocated and what
// the result of that was. // the result of that was.
type ResourceClaimStatus struct { type ResourceClaimStatus struct {

View File

@ -214,7 +214,7 @@ func (DeviceSelector) SwaggerDoc() map[string]string {
var map_OpaqueDeviceConfiguration = map[string]string{ var map_OpaqueDeviceConfiguration = map[string]string{
"": "OpaqueDeviceConfiguration contains configuration parameters for a driver in a format defined by the driver vendor.", "": "OpaqueDeviceConfiguration contains configuration parameters for a driver in a format defined by the driver vendor.",
"driver": "Driver is used to determine which kubelet plugin needs to be passed these configuration parameters.\n\nAn admission policy provided by the driver developer could use this to decide whether it needs to validate them.\n\nMust be a DNS subdomain and should end with a DNS domain owned by the vendor of the driver.", "driver": "Driver is used to determine which kubelet plugin needs to be passed these configuration parameters.\n\nAn admission policy provided by the driver developer could use this to decide whether it needs to validate them.\n\nMust be a DNS subdomain and should end with a DNS domain owned by the vendor of the driver.",
"parameters": "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions.", "parameters": "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions.\n\nThe length of the raw data must be smaller or equal to 10 Ki.",
} }
func (OpaqueDeviceConfiguration) SwaggerDoc() map[string]string { func (OpaqueDeviceConfiguration) SwaggerDoc() map[string]string {

View File

@ -508,6 +508,8 @@ message OpaqueDeviceConfiguration {
// includes self-identification and a version ("kind" + "apiVersion" for // includes self-identification and a version ("kind" + "apiVersion" for
// Kubernetes types), with conversion between different versions. // Kubernetes types), with conversion between different versions.
// //
// The length of the raw data must be smaller or equal to 10 Ki.
//
// +required // +required
optional .k8s.io.apimachinery.pkg.runtime.RawExtension parameters = 2; optional .k8s.io.apimachinery.pkg.runtime.RawExtension parameters = 2;
} }

View File

@ -660,10 +660,16 @@ type OpaqueDeviceConfiguration struct {
// includes self-identification and a version ("kind" + "apiVersion" for // includes self-identification and a version ("kind" + "apiVersion" for
// Kubernetes types), with conversion between different versions. // Kubernetes types), with conversion between different versions.
// //
// The length of the raw data must be smaller or equal to 10 Ki.
//
// +required // +required
Parameters runtime.RawExtension `json:"parameters" protobuf:"bytes,2,name=parameters"` Parameters runtime.RawExtension `json:"parameters" protobuf:"bytes,2,name=parameters"`
} }
// OpaqueParametersMaxLength is the maximum length of the raw data in an
// [OpaqueDeviceConfiguration.Parameters] field.
const OpaqueParametersMaxLength = 10 * 1024
// ResourceClaimStatus tracks whether the resource has been allocated and what // ResourceClaimStatus tracks whether the resource has been allocated and what
// the result of that was. // the result of that was.
type ResourceClaimStatus struct { type ResourceClaimStatus struct {

View File

@ -223,7 +223,7 @@ func (DeviceSelector) SwaggerDoc() map[string]string {
var map_OpaqueDeviceConfiguration = map[string]string{ var map_OpaqueDeviceConfiguration = map[string]string{
"": "OpaqueDeviceConfiguration contains configuration parameters for a driver in a format defined by the driver vendor.", "": "OpaqueDeviceConfiguration contains configuration parameters for a driver in a format defined by the driver vendor.",
"driver": "Driver is used to determine which kubelet plugin needs to be passed these configuration parameters.\n\nAn admission policy provided by the driver developer could use this to decide whether it needs to validate them.\n\nMust be a DNS subdomain and should end with a DNS domain owned by the vendor of the driver.", "driver": "Driver is used to determine which kubelet plugin needs to be passed these configuration parameters.\n\nAn admission policy provided by the driver developer could use this to decide whether it needs to validate them.\n\nMust be a DNS subdomain and should end with a DNS domain owned by the vendor of the driver.",
"parameters": "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions.", "parameters": "Parameters can contain arbitrary data. It is the responsibility of the driver developer to handle validation and versioning. Typically this includes self-identification and a version (\"kind\" + \"apiVersion\" for Kubernetes types), with conversion between different versions.\n\nThe length of the raw data must be smaller or equal to 10 Ki.",
} }
func (OpaqueDeviceConfiguration) SwaggerDoc() map[string]string { func (OpaqueDeviceConfiguration) SwaggerDoc() map[string]string {