mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-06-08 16:30:57 -04:00
Merge pull request #137291 from everettraven/upstream/dv/maxLength-fixup
[Declarative Validation] Bring `k8s:maxLength` tag in line with OpenAPI `maxLength` validation semantics
This commit is contained in:
commit
e08e598df0
33 changed files with 957 additions and 93 deletions
12
api/openapi-spec/swagger.json
generated
12
api/openapi-spec/swagger.json
generated
|
|
@ -16333,11 +16333,11 @@
|
|||
"description": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.",
|
||||
"properties": {
|
||||
"hardwareAddress": {
|
||||
"description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.",
|
||||
"description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.",
|
||||
"type": "string"
|
||||
},
|
||||
"interfaceName": {
|
||||
"description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",
|
||||
"description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.",
|
||||
"type": "string"
|
||||
},
|
||||
"ips": {
|
||||
|
|
@ -17667,11 +17667,11 @@
|
|||
"description": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.",
|
||||
"properties": {
|
||||
"hardwareAddress": {
|
||||
"description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.",
|
||||
"description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.",
|
||||
"type": "string"
|
||||
},
|
||||
"interfaceName": {
|
||||
"description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",
|
||||
"description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.",
|
||||
"type": "string"
|
||||
},
|
||||
"ips": {
|
||||
|
|
@ -18853,11 +18853,11 @@
|
|||
"description": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.",
|
||||
"properties": {
|
||||
"hardwareAddress": {
|
||||
"description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.",
|
||||
"description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.",
|
||||
"type": "string"
|
||||
},
|
||||
"interfaceName": {
|
||||
"description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",
|
||||
"description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.",
|
||||
"type": "string"
|
||||
},
|
||||
"ips": {
|
||||
|
|
|
|||
|
|
@ -1100,11 +1100,11 @@
|
|||
"description": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.",
|
||||
"properties": {
|
||||
"hardwareAddress": {
|
||||
"description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.",
|
||||
"description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.",
|
||||
"type": "string"
|
||||
},
|
||||
"interfaceName": {
|
||||
"description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",
|
||||
"description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.",
|
||||
"type": "string"
|
||||
},
|
||||
"ips": {
|
||||
|
|
|
|||
|
|
@ -1097,11 +1097,11 @@
|
|||
"description": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.",
|
||||
"properties": {
|
||||
"hardwareAddress": {
|
||||
"description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.",
|
||||
"description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.",
|
||||
"type": "string"
|
||||
},
|
||||
"interfaceName": {
|
||||
"description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",
|
||||
"description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.",
|
||||
"type": "string"
|
||||
},
|
||||
"ips": {
|
||||
|
|
|
|||
|
|
@ -1100,11 +1100,11 @@
|
|||
"description": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.",
|
||||
"properties": {
|
||||
"hardwareAddress": {
|
||||
"description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.",
|
||||
"description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.",
|
||||
"type": "string"
|
||||
},
|
||||
"interfaceName": {
|
||||
"description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",
|
||||
"description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.",
|
||||
"type": "string"
|
||||
},
|
||||
"ips": {
|
||||
|
|
|
|||
4
pkg/apis/resource/v1/zz_generated.validations.go
generated
4
pkg/apis/resource/v1/zz_generated.validations.go
generated
|
|
@ -1465,7 +1465,7 @@ func Validate_NetworkDeviceData(ctx context.Context, op operation.Operation, fld
|
|||
if earlyReturn {
|
||||
return // do not proceed
|
||||
}
|
||||
errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256).MarkAlpha()...)
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 256).MarkAlpha()...)
|
||||
return
|
||||
}(fldPath.Child("interfaceName"), &obj.InterfaceName, safe.Field(oldObj, func(oldObj *resourcev1.NetworkDeviceData) *string { return &oldObj.InterfaceName }), oldObj != nil)...)
|
||||
|
||||
|
|
@ -1508,7 +1508,7 @@ func Validate_NetworkDeviceData(ctx context.Context, op operation.Operation, fld
|
|||
if earlyReturn {
|
||||
return // do not proceed
|
||||
}
|
||||
errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 128).MarkAlpha()...)
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 128).MarkAlpha()...)
|
||||
return
|
||||
}(fldPath.Child("hardwareAddress"), &obj.HardwareAddress, safe.Field(oldObj, func(oldObj *resourcev1.NetworkDeviceData) *string { return &oldObj.HardwareAddress }), oldObj != nil)...)
|
||||
|
||||
|
|
|
|||
|
|
@ -1496,7 +1496,7 @@ func Validate_NetworkDeviceData(ctx context.Context, op operation.Operation, fld
|
|||
if earlyReturn {
|
||||
return // do not proceed
|
||||
}
|
||||
errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256).MarkAlpha()...)
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 256).MarkAlpha()...)
|
||||
return
|
||||
}(fldPath.Child("interfaceName"), &obj.InterfaceName, safe.Field(oldObj, func(oldObj *resourcev1beta1.NetworkDeviceData) *string { return &oldObj.InterfaceName }), oldObj != nil)...)
|
||||
|
||||
|
|
@ -1539,7 +1539,7 @@ func Validate_NetworkDeviceData(ctx context.Context, op operation.Operation, fld
|
|||
if earlyReturn {
|
||||
return // do not proceed
|
||||
}
|
||||
errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 128).MarkAlpha()...)
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 128).MarkAlpha()...)
|
||||
return
|
||||
}(fldPath.Child("hardwareAddress"), &obj.HardwareAddress, safe.Field(oldObj, func(oldObj *resourcev1beta1.NetworkDeviceData) *string { return &oldObj.HardwareAddress }), oldObj != nil)...)
|
||||
|
||||
|
|
|
|||
|
|
@ -1495,7 +1495,7 @@ func Validate_NetworkDeviceData(ctx context.Context, op operation.Operation, fld
|
|||
if earlyReturn {
|
||||
return // do not proceed
|
||||
}
|
||||
errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256).MarkAlpha()...)
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 256).MarkAlpha()...)
|
||||
return
|
||||
}(fldPath.Child("interfaceName"), &obj.InterfaceName, safe.Field(oldObj, func(oldObj *resourcev1beta2.NetworkDeviceData) *string { return &oldObj.InterfaceName }), oldObj != nil)...)
|
||||
|
||||
|
|
@ -1538,7 +1538,7 @@ func Validate_NetworkDeviceData(ctx context.Context, op operation.Operation, fld
|
|||
if earlyReturn {
|
||||
return // do not proceed
|
||||
}
|
||||
errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 128).MarkAlpha()...)
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 128).MarkAlpha()...)
|
||||
return
|
||||
}(fldPath.Child("hardwareAddress"), &obj.HardwareAddress, safe.Field(oldObj, func(oldObj *resourcev1beta2.NetworkDeviceData) *string { return &oldObj.HardwareAddress }), oldObj != nil)...)
|
||||
|
||||
|
|
|
|||
|
|
@ -1329,11 +1329,11 @@ func validateNetworkDeviceData(networkDeviceData *resource.NetworkDeviceData, fl
|
|||
}
|
||||
|
||||
if len(networkDeviceData.InterfaceName) > resource.NetworkDeviceDataInterfaceNameMaxLength {
|
||||
allErrs = append(allErrs, field.TooLong(fldPath.Child("interfaceName"), "" /* unused */, resource.NetworkDeviceDataInterfaceNameMaxLength).WithOrigin("maxLength").MarkCoveredByDeclarative())
|
||||
allErrs = append(allErrs, field.TooLong(fldPath.Child("interfaceName"), "" /* unused */, resource.NetworkDeviceDataInterfaceNameMaxLength).WithOrigin("maxBytes").MarkCoveredByDeclarative())
|
||||
}
|
||||
|
||||
if len(networkDeviceData.HardwareAddress) > resource.NetworkDeviceDataHardwareAddressMaxLength {
|
||||
allErrs = append(allErrs, field.TooLong(fldPath.Child("hardwareAddress"), "" /* unused */, resource.NetworkDeviceDataHardwareAddressMaxLength).WithOrigin("maxLength").MarkCoveredByDeclarative())
|
||||
allErrs = append(allErrs, field.TooLong(fldPath.Child("hardwareAddress"), "" /* unused */, resource.NetworkDeviceDataHardwareAddressMaxLength).WithOrigin("maxBytes").MarkCoveredByDeclarative())
|
||||
}
|
||||
|
||||
allErrs = append(allErrs, validateSet(networkDeviceData.IPs, resource.NetworkDeviceDataMaxIPs,
|
||||
|
|
|
|||
|
|
@ -1394,6 +1394,45 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
|
|||
},
|
||||
deviceStatusFeatureGate: true,
|
||||
},
|
||||
"valid-network-device-status-with-multi-byte-interface-name-and-hardware-address": {
|
||||
oldClaim: func() *resource.ResourceClaim { return validAllocatedClaim }(),
|
||||
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||
claim.Status.Devices = []resource.AllocatedDeviceStatus{
|
||||
{
|
||||
Driver: goodName,
|
||||
Pool: goodName,
|
||||
Device: goodName,
|
||||
Conditions: []metav1.Condition{
|
||||
{Type: "test-0", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0},
|
||||
{Type: "test-1", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0},
|
||||
{Type: "test-2", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0},
|
||||
{Type: "test-3", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0},
|
||||
{Type: "test-4", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0},
|
||||
{Type: "test-5", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0},
|
||||
{Type: "test-6", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0},
|
||||
{Type: "test-7", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0},
|
||||
},
|
||||
Data: &runtime.RawExtension{
|
||||
Raw: []byte(`{"kind": "foo", "apiVersion": "dra.example.com/v1"}`),
|
||||
},
|
||||
NetworkData: &resource.NetworkDeviceData{
|
||||
InterfaceName: strings.Repeat("𝄞", 256/4), // the G clef unicode character is exactly 4 bytes so repeating this 256/4 times means that it is exactly 256 bytes worth of length and should be valid.
|
||||
HardwareAddress: strings.Repeat("𝄞", 128/4), // the G clef unicode character is exactly 4 bytes so repeating this 128/4 times means that it is exactly 128 bytes worth of length and should be valid.
|
||||
IPs: []string{
|
||||
"10.9.8.0/24",
|
||||
"2001:db8::/64",
|
||||
"10.9.8.1/24",
|
||||
"2001:db8::1/64",
|
||||
"10.9.8.2/24", "10.9.8.3/24", "10.9.8.4/24", "10.9.8.5/24", "10.9.8.6/24", "10.9.8.7/24",
|
||||
"10.9.8.8/24", "10.9.8.9/24", "10.9.8.10/24", "10.9.8.11/24", "10.9.8.12/24", "10.9.8.13/24",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return claim
|
||||
},
|
||||
deviceStatusFeatureGate: true,
|
||||
},
|
||||
"invalid-device-status-duplicate": {
|
||||
wantFailures: field.ErrorList{
|
||||
field.Duplicate(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(1), "2001:db8::1/64").MarkCoveredByDeclarative(),
|
||||
|
|
@ -1483,6 +1522,36 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
|
|||
},
|
||||
deviceStatusFeatureGate: true,
|
||||
},
|
||||
"invalid-network-device-status-with-multi-byte-interface-name-and-hardware-address": {
|
||||
wantFailures: field.ErrorList{
|
||||
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "interfaceName"), "", resource.NetworkDeviceDataInterfaceNameMaxLength).MarkCoveredByDeclarative(),
|
||||
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "hardwareAddress"), "", resource.NetworkDeviceDataHardwareAddressMaxLength).MarkCoveredByDeclarative(),
|
||||
field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(0), "300.9.8.0/24", "must be a valid address in CIDR form, (e.g. 10.9.8.7/24 or 2001:db8::1/64)"),
|
||||
field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(1), "010.009.008.000/24", "must be in canonical form (\"10.9.8.0/24\")"),
|
||||
field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(2), "2001:0db8::1/64", "must be in canonical form (\"2001:db8::1/64\")"),
|
||||
},
|
||||
oldClaim: func() *resource.ResourceClaim { return validAllocatedClaim }(),
|
||||
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||
claim.Status.Devices = []resource.AllocatedDeviceStatus{
|
||||
{
|
||||
Driver: goodName,
|
||||
Pool: goodName,
|
||||
Device: goodName,
|
||||
NetworkData: &resource.NetworkDeviceData{
|
||||
InterfaceName: strings.Repeat("𝄞", resource.NetworkDeviceDataInterfaceNameMaxLength), // the G clef unicode character is exactly 4 bytes in length and should exceed the byte length limit for this field by a factor of 4
|
||||
HardwareAddress: strings.Repeat("𝄞", resource.NetworkDeviceDataHardwareAddressMaxLength), // the G clef unicode character is exactly 4 bytes in length and should exceed the byte length limit for this field by a factor of 4
|
||||
IPs: []string{
|
||||
"300.9.8.0/24",
|
||||
"010.009.008.000/24",
|
||||
"2001:0db8::1/64",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return claim
|
||||
},
|
||||
deviceStatusFeatureGate: true,
|
||||
},
|
||||
"invalid-data-device-status": {
|
||||
wantFailures: field.ErrorList{
|
||||
field.Invalid(field.NewPath("status", "devices").Index(0).Child("data"), "<value omitted>", "error parsing data as JSON: invalid character 'o' in literal false (expecting 'a')"),
|
||||
|
|
|
|||
12
pkg/generated/openapi/zz_generated.openapi.go
generated
12
pkg/generated/openapi/zz_generated.openapi.go
generated
|
|
@ -48248,7 +48248,7 @@ func schema_k8sio_api_resource_v1_NetworkDeviceData(ref common.ReferenceCallback
|
|||
Properties: map[string]spec.Schema{
|
||||
"interfaceName": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",
|
||||
Description: "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
|
|
@ -48275,7 +48275,7 @@ func schema_k8sio_api_resource_v1_NetworkDeviceData(ref common.ReferenceCallback
|
|||
},
|
||||
"hardwareAddress": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.",
|
||||
Description: "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
|
|
@ -50734,7 +50734,7 @@ func schema_k8sio_api_resource_v1beta1_NetworkDeviceData(ref common.ReferenceCal
|
|||
Properties: map[string]spec.Schema{
|
||||
"interfaceName": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",
|
||||
Description: "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
|
|
@ -50761,7 +50761,7 @@ func schema_k8sio_api_resource_v1beta1_NetworkDeviceData(ref common.ReferenceCal
|
|||
},
|
||||
"hardwareAddress": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.",
|
||||
Description: "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
|
|
@ -52928,7 +52928,7 @@ func schema_k8sio_api_resource_v1beta2_NetworkDeviceData(ref common.ReferenceCal
|
|||
Properties: map[string]spec.Schema{
|
||||
"interfaceName": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",
|
||||
Description: "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
|
|
@ -52955,7 +52955,7 @@ func schema_k8sio_api_resource_v1beta2_NetworkDeviceData(ref common.ReferenceCal
|
|||
},
|
||||
"hardwareAddress": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.",
|
||||
Description: "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1257,6 +1257,21 @@ func testValidateStatusUpdateForDeclarative(t *testing.T, apiVersion string) {
|
|||
),
|
||||
),
|
||||
},
|
||||
"valid networkdevicedata interfacename with multi-byte characters": {
|
||||
old: mkValidResourceClaim(),
|
||||
update: mkResourceClaimWithStatus(
|
||||
tweakStatusDevices(
|
||||
resource.AllocatedDeviceStatus{
|
||||
Driver: "dra.example.com",
|
||||
Pool: "pool-0",
|
||||
Device: "device-0",
|
||||
NetworkData: &resource.NetworkDeviceData{
|
||||
InterfaceName: strings.Repeat("𝄞", resource.NetworkDeviceDataInterfaceNameMaxLength/4), // the G clef unicode character is exactly 4 bytes so repeating this 256/4 times means that it is exactly 256 bytes worth of length and should be valid.
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
"invalid networkdevicedata interfacename too long": {
|
||||
old: mkValidResourceClaim(),
|
||||
update: mkResourceClaimWithStatus(
|
||||
|
|
@ -1272,7 +1287,28 @@ func testValidateStatusUpdateForDeclarative(t *testing.T, apiVersion string) {
|
|||
),
|
||||
),
|
||||
expectedErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "interfaceName"), "", resource.NetworkDeviceDataInterfaceNameMaxLength).MarkCoveredByDeclarative().WithOrigin("maxLength").MarkAlpha(),
|
||||
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "interfaceName"), "", resource.NetworkDeviceDataInterfaceNameMaxLength).MarkCoveredByDeclarative().WithOrigin("maxBytes").MarkAlpha(),
|
||||
},
|
||||
},
|
||||
"invalid networkdevicedata interfacename too long with multi-byte characters repeating max bytes length times": {
|
||||
old: mkValidResourceClaim(),
|
||||
update: mkResourceClaimWithStatus(
|
||||
tweakStatusDevices(
|
||||
resource.AllocatedDeviceStatus{
|
||||
Driver: "dra.example.com",
|
||||
Pool: "pool-0",
|
||||
Device: "device-0",
|
||||
NetworkData: &resource.NetworkDeviceData{
|
||||
// The G clef unicode character is exactly 4 bytes in length so repeating the character
|
||||
// the same number of times as the maxBytes payload means that we are guaranteed to fail
|
||||
// a byte-based length check, but not a character based check.
|
||||
InterfaceName: strings.Repeat("𝄞", resource.NetworkDeviceDataInterfaceNameMaxLength),
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
expectedErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "interfaceName"), "", resource.NetworkDeviceDataInterfaceNameMaxLength).MarkCoveredByDeclarative().WithOrigin("maxBytes").MarkAlpha(),
|
||||
},
|
||||
},
|
||||
"valid status.devices.networkData.hardwareAddress": {
|
||||
|
|
@ -1290,6 +1326,21 @@ func testValidateStatusUpdateForDeclarative(t *testing.T, apiVersion string) {
|
|||
),
|
||||
),
|
||||
},
|
||||
"valid status.devices.networkData.hardwareAddress with multi-byte characters": {
|
||||
old: mkValidResourceClaim(),
|
||||
update: mkResourceClaimWithStatus(
|
||||
tweakStatusDevices(
|
||||
resource.AllocatedDeviceStatus{
|
||||
Driver: "dra.example.com",
|
||||
Pool: "pool-0",
|
||||
Device: "device-0",
|
||||
NetworkData: &resource.NetworkDeviceData{
|
||||
HardwareAddress: strings.Repeat("𝄞", resource.NetworkDeviceDataHardwareAddressMaxLength/4), // the G clef unicode character is exactly 4 bytes so repeating this 256/4 times means that it is exactly 256 bytes worth of length and should be valid.
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
"invalid status.devices.networkData.hardwareAddress too long": {
|
||||
old: mkValidResourceClaim(),
|
||||
update: mkResourceClaimWithStatus(
|
||||
|
|
@ -1305,7 +1356,28 @@ func testValidateStatusUpdateForDeclarative(t *testing.T, apiVersion string) {
|
|||
),
|
||||
),
|
||||
expectedErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "hardwareAddress"), "", resource.NetworkDeviceDataHardwareAddressMaxLength).MarkCoveredByDeclarative().WithOrigin("maxLength").MarkAlpha(),
|
||||
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "hardwareAddress"), "", resource.NetworkDeviceDataHardwareAddressMaxLength).MarkCoveredByDeclarative().WithOrigin("maxBytes").MarkAlpha(),
|
||||
},
|
||||
},
|
||||
"invalid status.devices.networkData.hardwareAddress too long with multi-byte characters repeating max bytes length times": {
|
||||
old: mkValidResourceClaim(),
|
||||
update: mkResourceClaimWithStatus(
|
||||
tweakStatusDevices(
|
||||
resource.AllocatedDeviceStatus{
|
||||
Driver: "dra.example.com",
|
||||
Pool: "pool-0",
|
||||
Device: "device-0",
|
||||
NetworkData: &resource.NetworkDeviceData{
|
||||
// The G clef unicode character is exactly 4 bytes in length so repeating the character
|
||||
// the same number of times as the maxBytes payload means that we are guaranteed to fail
|
||||
// a byte-based length check, but not a character based check.
|
||||
HardwareAddress: strings.Repeat("𝄞", resource.NetworkDeviceDataHardwareAddressMaxLength),
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
expectedErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "hardwareAddress"), "", resource.NetworkDeviceDataHardwareAddressMaxLength).MarkCoveredByDeclarative().WithOrigin("maxBytes").MarkAlpha(),
|
||||
},
|
||||
},
|
||||
"invalid status.devices.networkData.ips duplicate": {
|
||||
|
|
|
|||
|
|
@ -1319,11 +1319,11 @@ message NetworkDeviceData {
|
|||
// the allocated device. This might be the name of a physical or virtual
|
||||
// network interface being configured in the pod.
|
||||
//
|
||||
// Must not be longer than 256 characters.
|
||||
// Must not be longer than 256 bytes.
|
||||
//
|
||||
// +optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxLength=256
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxBytes=256
|
||||
optional string interfaceName = 1;
|
||||
|
||||
// IPs lists the network addresses assigned to the device's network interface.
|
||||
|
|
@ -1342,11 +1342,11 @@ message NetworkDeviceData {
|
|||
|
||||
// HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.
|
||||
//
|
||||
// Must not be longer than 128 characters.
|
||||
// Must not be longer than 128 bytes.
|
||||
//
|
||||
// +optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxLength=128
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxBytes=128
|
||||
optional string hardwareAddress = 3;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1998,11 +1998,11 @@ type NetworkDeviceData struct {
|
|||
// the allocated device. This might be the name of a physical or virtual
|
||||
// network interface being configured in the pod.
|
||||
//
|
||||
// Must not be longer than 256 characters.
|
||||
// Must not be longer than 256 bytes.
|
||||
//
|
||||
// +optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxLength=256
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxBytes=256
|
||||
InterfaceName string `json:"interfaceName,omitempty" protobuf:"bytes,1,opt,name=interfaceName"`
|
||||
|
||||
// IPs lists the network addresses assigned to the device's network interface.
|
||||
|
|
@ -2021,10 +2021,10 @@ type NetworkDeviceData struct {
|
|||
|
||||
// HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.
|
||||
//
|
||||
// Must not be longer than 128 characters.
|
||||
// Must not be longer than 128 bytes.
|
||||
//
|
||||
// +optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxLength=128
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxBytes=128
|
||||
HardwareAddress string `json:"hardwareAddress,omitempty" protobuf:"bytes,3,opt,name=hardwareAddress"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -358,9 +358,9 @@ func (ExactDeviceRequest) SwaggerDoc() map[string]string {
|
|||
|
||||
var map_NetworkDeviceData = map[string]string{
|
||||
"": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.",
|
||||
"interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",
|
||||
"interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.",
|
||||
"ips": "IPs lists the network addresses assigned to the device's network interface. This can include both IPv4 and IPv6 addresses. The IPs are in the CIDR notation, which includes both the address and the associated subnet mask. e.g.: \"192.0.2.5/24\" for IPv4 and \"2001:db8::5/64\" for IPv6.",
|
||||
"hardwareAddress": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.",
|
||||
"hardwareAddress": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.",
|
||||
}
|
||||
|
||||
func (NetworkDeviceData) SwaggerDoc() map[string]string {
|
||||
|
|
|
|||
|
|
@ -1335,11 +1335,11 @@ message NetworkDeviceData {
|
|||
// the allocated device. This might be the name of a physical or virtual
|
||||
// network interface being configured in the pod.
|
||||
//
|
||||
// Must not be longer than 256 characters.
|
||||
// Must not be longer than 256 bytes.
|
||||
//
|
||||
// +optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxLength=256
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxBytes=256
|
||||
optional string interfaceName = 1;
|
||||
|
||||
// IPs lists the network addresses assigned to the device's network interface.
|
||||
|
|
@ -1360,11 +1360,11 @@ message NetworkDeviceData {
|
|||
|
||||
// HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.
|
||||
//
|
||||
// Must not be longer than 128 characters.
|
||||
// Must not be longer than 128 bytes.
|
||||
//
|
||||
// +optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxLength=128
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxBytes=128
|
||||
optional string hardwareAddress = 3;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2023,11 +2023,11 @@ type NetworkDeviceData struct {
|
|||
// the allocated device. This might be the name of a physical or virtual
|
||||
// network interface being configured in the pod.
|
||||
//
|
||||
// Must not be longer than 256 characters.
|
||||
// Must not be longer than 256 bytes.
|
||||
//
|
||||
// +optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxLength=256
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxBytes=256
|
||||
InterfaceName string `json:"interfaceName,omitempty" protobuf:"bytes,1,opt,name=interfaceName"`
|
||||
|
||||
// IPs lists the network addresses assigned to the device's network interface.
|
||||
|
|
@ -2048,10 +2048,10 @@ type NetworkDeviceData struct {
|
|||
|
||||
// HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.
|
||||
//
|
||||
// Must not be longer than 128 characters.
|
||||
// Must not be longer than 128 bytes.
|
||||
//
|
||||
// +optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxLength=128
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxBytes=128
|
||||
HardwareAddress string `json:"hardwareAddress,omitempty" protobuf:"bytes,3,opt,name=hardwareAddress"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -358,9 +358,9 @@ func (DeviceToleration) SwaggerDoc() map[string]string {
|
|||
|
||||
var map_NetworkDeviceData = map[string]string{
|
||||
"": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.",
|
||||
"interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",
|
||||
"interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.",
|
||||
"ips": "IPs lists the network addresses assigned to the device's network interface. This can include both IPv4 and IPv6 addresses. The IPs are in the CIDR notation, which includes both the address and the associated subnet mask. e.g.: \"192.0.2.5/24\" for IPv4 and \"2001:db8::5/64\" for IPv6.\n\nMust not contain more than 16 entries.",
|
||||
"hardwareAddress": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.",
|
||||
"hardwareAddress": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.",
|
||||
}
|
||||
|
||||
func (NetworkDeviceData) SwaggerDoc() map[string]string {
|
||||
|
|
|
|||
|
|
@ -1322,11 +1322,11 @@ message NetworkDeviceData {
|
|||
// the allocated device. This might be the name of a physical or virtual
|
||||
// network interface being configured in the pod.
|
||||
//
|
||||
// Must not be longer than 256 characters.
|
||||
// Must not be longer than 256 bytes.
|
||||
//
|
||||
// +optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxLength=256
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxBytes=256
|
||||
optional string interfaceName = 1;
|
||||
|
||||
// IPs lists the network addresses assigned to the device's network interface.
|
||||
|
|
@ -1345,11 +1345,11 @@ message NetworkDeviceData {
|
|||
|
||||
// HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.
|
||||
//
|
||||
// Must not be longer than 128 characters.
|
||||
// Must not be longer than 128 bytes.
|
||||
//
|
||||
// +optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxLength=128
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxBytes=128
|
||||
optional string hardwareAddress = 3;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2010,11 +2010,11 @@ type NetworkDeviceData struct {
|
|||
// the allocated device. This might be the name of a physical or virtual
|
||||
// network interface being configured in the pod.
|
||||
//
|
||||
// Must not be longer than 256 characters.
|
||||
// Must not be longer than 256 bytes.
|
||||
//
|
||||
// +optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxLength=256
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxBytes=256
|
||||
InterfaceName string `json:"interfaceName,omitempty" protobuf:"bytes,1,opt,name=interfaceName"`
|
||||
|
||||
// IPs lists the network addresses assigned to the device's network interface.
|
||||
|
|
@ -2033,10 +2033,10 @@ type NetworkDeviceData struct {
|
|||
|
||||
// HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.
|
||||
//
|
||||
// Must not be longer than 128 characters.
|
||||
// Must not be longer than 128 bytes.
|
||||
//
|
||||
// +optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:optional
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxLength=128
|
||||
// +k8s:alpha(since: "1.36")=+k8s:maxBytes=128
|
||||
HardwareAddress string `json:"hardwareAddress,omitempty" protobuf:"bytes,3,opt,name=hardwareAddress"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -358,9 +358,9 @@ func (ExactDeviceRequest) SwaggerDoc() map[string]string {
|
|||
|
||||
var map_NetworkDeviceData = map[string]string{
|
||||
"": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.",
|
||||
"interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",
|
||||
"interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.",
|
||||
"ips": "IPs lists the network addresses assigned to the device's network interface. This can include both IPv4 and IPv6 addresses. The IPs are in the CIDR notation, which includes both the address and the associated subnet mask. e.g.: \"192.0.2.5/24\" for IPv4 and \"2001:db8::5/64\" for IPv6.",
|
||||
"hardwareAddress": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.",
|
||||
"hardwareAddress": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.",
|
||||
}
|
||||
|
||||
func (NetworkDeviceData) SwaggerDoc() map[string]string {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ package validate
|
|||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"unicode/utf8"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/api/validate/constraints"
|
||||
|
|
@ -31,9 +33,40 @@ func MaxLength[T ~string](_ context.Context, _ operation.Operation, fldPath *fie
|
|||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
if len(*value) > max {
|
||||
return field.ErrorList{field.TooLong(fldPath, *value, max).WithOrigin("maxLength")}
|
||||
|
||||
// if the length of the value in bytes is less
|
||||
// than the maximum size then we can confidently
|
||||
// say that this value is within the bounds
|
||||
// enforced by the maximum value regardless
|
||||
// of the actual makeup of characters in the value
|
||||
byteLength := len(*value)
|
||||
if byteLength <= max {
|
||||
return nil
|
||||
}
|
||||
|
||||
// because runes are up to 4 byte characters, if we assume all characters
|
||||
// in the input are runes, the minimum number of characters that
|
||||
// are specified is len(value)/4. If the minimum multi-byte
|
||||
// character count is greater than our enforced maximum, we
|
||||
// can confidently say that the value is invalid without having
|
||||
// to actually perform the more expensive rune counting step
|
||||
minimum := int(math.Ceil(float64(byteLength) / 4.0))
|
||||
if minimum > max || utf8.RuneCountInString(string(*value)) > max {
|
||||
return field.ErrorList{field.TooLongCharacters(fldPath, *value, max).WithOrigin("maxLength")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MaxBytes verifies that the specified value is not longer than max bytes.
|
||||
func MaxBytes[T ~string](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, max int) field.ErrorList {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(*value) > max {
|
||||
return field.ErrorList{field.TooLong(fldPath, *value, max).WithOrigin("maxBytes")}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func TestMaxLength(t *testing.T) {
|
|||
value: "0",
|
||||
max: 0,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), nil, 0).WithOrigin("maxLength"),
|
||||
field.TooLongCharacters(field.NewPath("fldpath"), "", 0).WithOrigin("maxLength"),
|
||||
},
|
||||
}, {
|
||||
name: "one character",
|
||||
|
|
@ -54,14 +54,55 @@ func TestMaxLength(t *testing.T) {
|
|||
value: "01",
|
||||
max: 1,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), nil, 1).WithOrigin("maxLength"),
|
||||
field.TooLongCharacters(field.NewPath("fldpath"), "", 1).WithOrigin("maxLength"),
|
||||
},
|
||||
}, {
|
||||
value: "",
|
||||
max: -1,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), nil, -1).WithOrigin("maxLength"),
|
||||
field.TooLongCharacters(field.NewPath("fldpath"), "", -1).WithOrigin("maxLength"),
|
||||
},
|
||||
}, {
|
||||
name: "ascii-only characters, less characters than max (n-1)",
|
||||
value: "abcdefghi",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "multi-byte characters, less characters than max (n-1)",
|
||||
value: "©®©®©®©®©",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "ascii-only characters, more characters than max (n+1)",
|
||||
value: "abcdefghijkl",
|
||||
max: 10,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLongCharacters(field.NewPath("fldpath"), "", 10).WithOrigin("maxLength"),
|
||||
},
|
||||
}, {
|
||||
name: "multi-byte characters, more characters than max (n+1)",
|
||||
value: "©®©®©®©®©®©",
|
||||
max: 10,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLongCharacters(field.NewPath("fldpath"), "", 10).WithOrigin("maxLength"),
|
||||
},
|
||||
}, {
|
||||
name: "mixture of characters, minimum possible size of input is less than max, rune count exceed maximum",
|
||||
value: "©abc®defghi",
|
||||
max: 10,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLongCharacters(field.NewPath("fldpath"), "", 10).WithOrigin("maxLength"),
|
||||
},
|
||||
}, {
|
||||
name: "multi-byte characters, exact characters as max (n)",
|
||||
value: "©®©®©®©®©®",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "ascii-only characters, exact characters as max (n)",
|
||||
value: "abcdefghij",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}}
|
||||
|
||||
matcher := field.ErrorMatcher{}.ByOrigin().ByDetailSubstring().ByField().ByType()
|
||||
|
|
@ -213,3 +254,87 @@ func doTestMinimum[T constraints.Integer](t *testing.T, cases []minimumTestCase[
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxBytes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
value string
|
||||
max int
|
||||
wantErrs field.ErrorList // regex
|
||||
}{{
|
||||
name: "empty string",
|
||||
value: "",
|
||||
max: 0,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "zero length",
|
||||
value: "0",
|
||||
max: 0,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), "", 0).WithOrigin("maxBytes"),
|
||||
},
|
||||
}, {
|
||||
name: "one character",
|
||||
value: "0",
|
||||
max: 1,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "two characters",
|
||||
value: "01",
|
||||
max: 1,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), "", 1).WithOrigin("maxBytes"),
|
||||
},
|
||||
}, {
|
||||
value: "",
|
||||
max: -1,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), "", -1).WithOrigin("maxBytes"),
|
||||
},
|
||||
}, {
|
||||
name: "ascii-only characters, less bytes than max",
|
||||
value: "abcdefghi",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "multi-byte characters, less bytes than max",
|
||||
value: "©®©®",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "ascii-only characters, more bytes than max",
|
||||
value: "abcdefghijkl",
|
||||
max: 10,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), "", 10).WithOrigin("maxBytes"),
|
||||
},
|
||||
}, {
|
||||
name: "multi-byte characters, more bytes than max",
|
||||
value: "©®©®©©",
|
||||
max: 10,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), "", 10).WithOrigin("maxBytes"),
|
||||
},
|
||||
}, {
|
||||
name: "mixture of characters, less bytes than max",
|
||||
value: "©abc®®",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "mixture of characters, more bytes than max",
|
||||
value: "©abc®®abc",
|
||||
max: 10,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), "", 10).WithOrigin("maxBytes"),
|
||||
},
|
||||
}}
|
||||
|
||||
matcher := field.ErrorMatcher{}.ByOrigin().ByDetailSubstring().ByField().ByType()
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
v := tc.value
|
||||
gotErrs := MaxBytes(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &v, nil, tc.max)
|
||||
matcher.Test(t, tc.wantErrs, gotErrs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -346,6 +346,29 @@ func TooLong(field *Path, _ interface{}, maxLength int) *Error {
|
|||
}
|
||||
}
|
||||
|
||||
// TooLongCharacters returns a *Error indicating "too long". This is used to report that
|
||||
// the given value is too long in characters (including multi-byte characters).
|
||||
// This is similar to Invalid, but the returned error will not include the too-long value.
|
||||
// If maxLength is negative, it will be included in the message. The value argument is not used.
|
||||
func TooLongCharacters[T ~string](field *Path, _ T, maxLength int) *Error {
|
||||
var msg string
|
||||
if maxLength >= 0 {
|
||||
bs := "chars"
|
||||
if maxLength == 1 {
|
||||
bs = "char"
|
||||
}
|
||||
msg = fmt.Sprintf("may not be more than %d %s", maxLength, bs)
|
||||
} else {
|
||||
msg = "value is too long"
|
||||
}
|
||||
return &Error{
|
||||
Type: ErrorTypeTooLong,
|
||||
Field: field.String(),
|
||||
BadValue: "<value omitted>",
|
||||
Detail: msg,
|
||||
}
|
||||
}
|
||||
|
||||
// TooLongMaxLength returns a *Error indicating "too long".
|
||||
// Deprecated: Use TooLong instead.
|
||||
func TooLongMaxLength(field *Path, value interface{}, maxLength int) *Error {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ type NetworkDeviceDataApplyConfiguration struct {
|
|||
// the allocated device. This might be the name of a physical or virtual
|
||||
// network interface being configured in the pod.
|
||||
//
|
||||
// Must not be longer than 256 characters.
|
||||
// Must not be longer than 256 bytes.
|
||||
InterfaceName *string `json:"interfaceName,omitempty"`
|
||||
// IPs lists the network addresses assigned to the device's network interface.
|
||||
// This can include both IPv4 and IPv6 addresses.
|
||||
|
|
@ -39,7 +39,7 @@ type NetworkDeviceDataApplyConfiguration struct {
|
|||
IPs []string `json:"ips,omitempty"`
|
||||
// HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.
|
||||
//
|
||||
// Must not be longer than 128 characters.
|
||||
// Must not be longer than 128 bytes.
|
||||
HardwareAddress *string `json:"hardwareAddress,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ type NetworkDeviceDataApplyConfiguration struct {
|
|||
// the allocated device. This might be the name of a physical or virtual
|
||||
// network interface being configured in the pod.
|
||||
//
|
||||
// Must not be longer than 256 characters.
|
||||
// Must not be longer than 256 bytes.
|
||||
InterfaceName *string `json:"interfaceName,omitempty"`
|
||||
// IPs lists the network addresses assigned to the device's network interface.
|
||||
// This can include both IPv4 and IPv6 addresses.
|
||||
|
|
@ -41,7 +41,7 @@ type NetworkDeviceDataApplyConfiguration struct {
|
|||
IPs []string `json:"ips,omitempty"`
|
||||
// HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.
|
||||
//
|
||||
// Must not be longer than 128 characters.
|
||||
// Must not be longer than 128 bytes.
|
||||
HardwareAddress *string `json:"hardwareAddress,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ type NetworkDeviceDataApplyConfiguration struct {
|
|||
// the allocated device. This might be the name of a physical or virtual
|
||||
// network interface being configured in the pod.
|
||||
//
|
||||
// Must not be longer than 256 characters.
|
||||
// Must not be longer than 256 bytes.
|
||||
InterfaceName *string `json:"interfaceName,omitempty"`
|
||||
// IPs lists the network addresses assigned to the device's network interface.
|
||||
// This can include both IPv4 and IPv6 addresses.
|
||||
|
|
@ -39,7 +39,7 @@ type NetworkDeviceDataApplyConfiguration struct {
|
|||
IPs []string `json:"ips,omitempty"`
|
||||
// HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.
|
||||
//
|
||||
// Must not be longer than 128 characters.
|
||||
// Must not be longer than 128 bytes.
|
||||
HardwareAddress *string `json:"hardwareAddress,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// +k8s:validation-gen=TypeMeta
|
||||
// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme
|
||||
|
||||
// This is a test package.
|
||||
// +k8s:validation-gen-nolint
|
||||
package maxbytes
|
||||
|
||||
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
|
||||
|
||||
var localSchemeBuilder = testscheme.New()
|
||||
|
||||
type Struct struct {
|
||||
TypeMeta int
|
||||
|
||||
// +k8s:maxBytes=0
|
||||
Max0Field string `json:"max0Field"`
|
||||
|
||||
// +k8s:maxBytes=0
|
||||
Max0PtrField *string `json:"max0PtrField"`
|
||||
|
||||
// +k8s:maxBytes=10
|
||||
Max10Field string `json:"max10Field"`
|
||||
|
||||
// +k8s:maxBytes=10
|
||||
Max10PtrField *string `json:"max10PtrField"`
|
||||
|
||||
// +k8s:maxBytes=0
|
||||
Max0UnvalidatedTypedefField UnvalidatedStringType `json:"max0UnvalidatedTypedefField"`
|
||||
|
||||
// +k8s:maxBytes=0
|
||||
Max0UnvalidatedTypedefPtrField *UnvalidatedStringType `json:"max0UnvalidatedTypedefPtrField"`
|
||||
|
||||
// +k8s:maxBytes=10
|
||||
Max10UnvalidatedTypedefField UnvalidatedStringType `json:"max10UnvalidatedTypedefField"`
|
||||
|
||||
// +k8s:maxBytes=10
|
||||
Max10UnvalidatedTypedefPtrField *UnvalidatedStringType `json:"max10UnvalidatedTypedefPtrField"`
|
||||
|
||||
// Note: no validation here
|
||||
Max0ValidatedTypedefField Max0Type `json:"max0ValidatedTypedefField"`
|
||||
|
||||
// Note: no validation here
|
||||
Max0ValidatedTypedefPtrField *Max0Type `json:"max0ValidatedTypedefPtrField"`
|
||||
|
||||
// Note: no validation here
|
||||
Max10ValidatedTypedefField Max10Type `json:"max10ValidatedTypedefField"`
|
||||
|
||||
// Note: no validation here
|
||||
Max10ValidatedTypedefPtrField *Max10Type `json:"max10ValidatedTypedefPtrField"`
|
||||
}
|
||||
|
||||
// Note: no validation here
|
||||
type UnvalidatedStringType string
|
||||
|
||||
// This tests that markers on type definitions
|
||||
// are pulled through the validations of fields
|
||||
// that use the type definition.
|
||||
// +k8s:maxBytes=0
|
||||
type Max0Type string
|
||||
|
||||
// This tests that markers on type definitions
|
||||
// are pulled through the validations of fields
|
||||
// that use the type definition.
|
||||
// +k8s:maxBytes=10
|
||||
type Max10Type string
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
Copyright 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 maxbytes
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
st := localSchemeBuilder.Test(t)
|
||||
|
||||
st.Value(&Struct{
|
||||
// All zero values
|
||||
}).ExpectValid()
|
||||
|
||||
st.Value(&Struct{
|
||||
Max10Field: strings.Repeat("x", 1),
|
||||
Max10PtrField: ptr.To(strings.Repeat("x", 1)),
|
||||
Max10UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 1)),
|
||||
Max10UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 1))),
|
||||
Max10ValidatedTypedefField: Max10Type(strings.Repeat("x", 1)),
|
||||
Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 1))),
|
||||
}).ExpectValid()
|
||||
|
||||
st.Value(&Struct{
|
||||
Max10Field: strings.Repeat("x", 9),
|
||||
Max10PtrField: ptr.To(strings.Repeat("x", 9)),
|
||||
Max10UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 9)),
|
||||
Max10UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 9))),
|
||||
Max10ValidatedTypedefField: Max10Type(strings.Repeat("x", 9)),
|
||||
Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 9))),
|
||||
}).ExpectValid()
|
||||
|
||||
st.Value(&Struct{
|
||||
Max10Field: strings.Repeat("x", 10),
|
||||
Max10PtrField: ptr.To(strings.Repeat("x", 10)),
|
||||
Max10UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 10)),
|
||||
Max10UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 10))),
|
||||
Max10ValidatedTypedefField: Max10Type(strings.Repeat("x", 10)),
|
||||
Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 10))),
|
||||
}).ExpectValid()
|
||||
|
||||
testVal := &Struct{
|
||||
Max0Field: strings.Repeat("x", 1),
|
||||
Max0PtrField: ptr.To(strings.Repeat("x", 1)),
|
||||
Max10Field: strings.Repeat("x", 11),
|
||||
Max10PtrField: ptr.To(strings.Repeat("x", 11)),
|
||||
Max0UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 1)),
|
||||
Max0UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 1))),
|
||||
Max10UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 11)),
|
||||
Max10UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 11))),
|
||||
Max0ValidatedTypedefField: Max0Type(strings.Repeat("x", 1)),
|
||||
Max0ValidatedTypedefPtrField: ptr.To(Max0Type(strings.Repeat("x", 1))),
|
||||
Max10ValidatedTypedefField: Max10Type(strings.Repeat("x", 11)),
|
||||
Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 11))),
|
||||
}
|
||||
st.Value(testVal).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.TooLong(field.NewPath("max0Field"), "", 0),
|
||||
field.TooLong(field.NewPath("max0PtrField"), "", 0),
|
||||
field.TooLong(field.NewPath("max10Field"), "", 10),
|
||||
field.TooLong(field.NewPath("max10PtrField"), "", 10),
|
||||
field.TooLong(field.NewPath("max0UnvalidatedTypedefField"), "", 0),
|
||||
field.TooLong(field.NewPath("max0UnvalidatedTypedefPtrField"), "", 0),
|
||||
field.TooLong(field.NewPath("max10UnvalidatedTypedefField"), "", 10),
|
||||
field.TooLong(field.NewPath("max10UnvalidatedTypedefPtrField"), "", 10),
|
||||
field.TooLong(field.NewPath("max0ValidatedTypedefField"), "", 0),
|
||||
field.TooLong(field.NewPath("max0ValidatedTypedefPtrField"), "", 0),
|
||||
field.TooLong(field.NewPath("max10ValidatedTypedefField"), "", 10),
|
||||
field.TooLong(field.NewPath("max10ValidatedTypedefPtrField"), "", 10),
|
||||
})
|
||||
|
||||
// Test validation ratcheting
|
||||
st.Value(&Struct{
|
||||
Max0Field: strings.Repeat("x", 1),
|
||||
Max0PtrField: ptr.To(strings.Repeat("x", 1)),
|
||||
Max10Field: strings.Repeat("x", 11),
|
||||
Max10PtrField: ptr.To(strings.Repeat("x", 11)),
|
||||
Max0UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 1)),
|
||||
Max0UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 1))),
|
||||
Max10UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 11)),
|
||||
Max10UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 11))),
|
||||
Max0ValidatedTypedefField: Max0Type(strings.Repeat("x", 1)),
|
||||
Max0ValidatedTypedefPtrField: ptr.To(Max0Type(strings.Repeat("x", 1))),
|
||||
Max10ValidatedTypedefField: Max10Type(strings.Repeat("x", 11)),
|
||||
Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 11))),
|
||||
}).OldValue(&Struct{
|
||||
Max0Field: strings.Repeat("x", 1),
|
||||
Max0PtrField: ptr.To(strings.Repeat("x", 1)),
|
||||
Max10Field: strings.Repeat("x", 11),
|
||||
Max10PtrField: ptr.To(strings.Repeat("x", 11)),
|
||||
Max0UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 1)),
|
||||
Max0UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 1))),
|
||||
Max10UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 11)),
|
||||
Max10UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 11))),
|
||||
Max0ValidatedTypedefField: Max0Type(strings.Repeat("x", 1)),
|
||||
Max0ValidatedTypedefPtrField: ptr.To(Max0Type(strings.Repeat("x", 1))),
|
||||
Max10ValidatedTypedefField: Max10Type(strings.Repeat("x", 11)),
|
||||
Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 11))),
|
||||
}).ExpectValid()
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by validation-gen. DO NOT EDIT.
|
||||
|
||||
package maxbytes
|
||||
|
||||
import (
|
||||
context "context"
|
||||
fmt "fmt"
|
||||
|
||||
operation "k8s.io/apimachinery/pkg/api/operation"
|
||||
safe "k8s.io/apimachinery/pkg/api/safe"
|
||||
validate "k8s.io/apimachinery/pkg/api/validate"
|
||||
field "k8s.io/apimachinery/pkg/util/validation/field"
|
||||
testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme"
|
||||
)
|
||||
|
||||
func init() { localSchemeBuilder.Register(RegisterValidations) }
|
||||
|
||||
// RegisterValidations adds validation functions to the given scheme.
|
||||
// Public to allow building arbitrary schemes.
|
||||
func RegisterValidations(scheme *testscheme.Scheme) error {
|
||||
// type Struct
|
||||
scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
|
||||
switch op.Request.SubresourcePath() {
|
||||
case "/":
|
||||
return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj))
|
||||
}
|
||||
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate_Max0Type validates an instance of Max0Type according
|
||||
// to declarative validation rules in the API schema.
|
||||
func Validate_Max0Type(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Max0Type) (errs field.ErrorList) {
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 0)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// Validate_Max10Type validates an instance of Max10Type according
|
||||
// to declarative validation rules in the API schema.
|
||||
func Validate_Max10Type(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Max10Type) (errs field.ErrorList) {
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 10)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// Validate_Struct validates an instance of Struct according
|
||||
// to declarative validation rules in the API schema.
|
||||
func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) {
|
||||
// field Struct.TypeMeta has no validation
|
||||
|
||||
// field Struct.Max0Field
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) {
|
||||
// don't revalidate unchanged data
|
||||
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 0)...)
|
||||
return
|
||||
}(fldPath.Child("max0Field"), &obj.Max0Field, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.Max0Field }), oldObj != nil)...)
|
||||
|
||||
// field Struct.Max0PtrField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) {
|
||||
// don't revalidate unchanged data
|
||||
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 0)...)
|
||||
return
|
||||
}(fldPath.Child("max0PtrField"), obj.Max0PtrField, safe.Field(oldObj, func(oldObj *Struct) *string { return oldObj.Max0PtrField }), oldObj != nil)...)
|
||||
|
||||
// field Struct.Max10Field
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) {
|
||||
// don't revalidate unchanged data
|
||||
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 10)...)
|
||||
return
|
||||
}(fldPath.Child("max10Field"), &obj.Max10Field, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.Max10Field }), oldObj != nil)...)
|
||||
|
||||
// field Struct.Max10PtrField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) {
|
||||
// don't revalidate unchanged data
|
||||
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 10)...)
|
||||
return
|
||||
}(fldPath.Child("max10PtrField"), obj.Max10PtrField, safe.Field(oldObj, func(oldObj *Struct) *string { return oldObj.Max10PtrField }), oldObj != nil)...)
|
||||
|
||||
// field Struct.Max0UnvalidatedTypedefField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *UnvalidatedStringType, oldValueCorrelated bool) (errs field.ErrorList) {
|
||||
// don't revalidate unchanged data
|
||||
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 0)...)
|
||||
return
|
||||
}(fldPath.Child("max0UnvalidatedTypedefField"), &obj.Max0UnvalidatedTypedefField, safe.Field(oldObj, func(oldObj *Struct) *UnvalidatedStringType { return &oldObj.Max0UnvalidatedTypedefField }), oldObj != nil)...)
|
||||
|
||||
// field Struct.Max0UnvalidatedTypedefPtrField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *UnvalidatedStringType, oldValueCorrelated bool) (errs field.ErrorList) {
|
||||
// don't revalidate unchanged data
|
||||
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 0)...)
|
||||
return
|
||||
}(fldPath.Child("max0UnvalidatedTypedefPtrField"), obj.Max0UnvalidatedTypedefPtrField, safe.Field(oldObj, func(oldObj *Struct) *UnvalidatedStringType { return oldObj.Max0UnvalidatedTypedefPtrField }), oldObj != nil)...)
|
||||
|
||||
// field Struct.Max10UnvalidatedTypedefField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *UnvalidatedStringType, oldValueCorrelated bool) (errs field.ErrorList) {
|
||||
// don't revalidate unchanged data
|
||||
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 10)...)
|
||||
return
|
||||
}(fldPath.Child("max10UnvalidatedTypedefField"), &obj.Max10UnvalidatedTypedefField, safe.Field(oldObj, func(oldObj *Struct) *UnvalidatedStringType { return &oldObj.Max10UnvalidatedTypedefField }), oldObj != nil)...)
|
||||
|
||||
// field Struct.Max10UnvalidatedTypedefPtrField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *UnvalidatedStringType, oldValueCorrelated bool) (errs field.ErrorList) {
|
||||
// don't revalidate unchanged data
|
||||
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil
|
||||
}
|
||||
// call field-attached validations
|
||||
errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 10)...)
|
||||
return
|
||||
}(fldPath.Child("max10UnvalidatedTypedefPtrField"), obj.Max10UnvalidatedTypedefPtrField, safe.Field(oldObj, func(oldObj *Struct) *UnvalidatedStringType { return oldObj.Max10UnvalidatedTypedefPtrField }), oldObj != nil)...)
|
||||
|
||||
// field Struct.Max0ValidatedTypedefField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *Max0Type, oldValueCorrelated bool) (errs field.ErrorList) {
|
||||
// don't revalidate unchanged data
|
||||
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil
|
||||
}
|
||||
// call the type's validation function
|
||||
errs = append(errs, Validate_Max0Type(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("max0ValidatedTypedefField"), &obj.Max0ValidatedTypedefField, safe.Field(oldObj, func(oldObj *Struct) *Max0Type { return &oldObj.Max0ValidatedTypedefField }), oldObj != nil)...)
|
||||
|
||||
// field Struct.Max0ValidatedTypedefPtrField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *Max0Type, oldValueCorrelated bool) (errs field.ErrorList) {
|
||||
// don't revalidate unchanged data
|
||||
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil
|
||||
}
|
||||
// call the type's validation function
|
||||
errs = append(errs, Validate_Max0Type(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("max0ValidatedTypedefPtrField"), obj.Max0ValidatedTypedefPtrField, safe.Field(oldObj, func(oldObj *Struct) *Max0Type { return oldObj.Max0ValidatedTypedefPtrField }), oldObj != nil)...)
|
||||
|
||||
// field Struct.Max10ValidatedTypedefField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *Max10Type, oldValueCorrelated bool) (errs field.ErrorList) {
|
||||
// don't revalidate unchanged data
|
||||
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil
|
||||
}
|
||||
// call the type's validation function
|
||||
errs = append(errs, Validate_Max10Type(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("max10ValidatedTypedefField"), &obj.Max10ValidatedTypedefField, safe.Field(oldObj, func(oldObj *Struct) *Max10Type { return &oldObj.Max10ValidatedTypedefField }), oldObj != nil)...)
|
||||
|
||||
// field Struct.Max10ValidatedTypedefPtrField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *Max10Type, oldValueCorrelated bool) (errs field.ErrorList) {
|
||||
// don't revalidate unchanged data
|
||||
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil
|
||||
}
|
||||
// call the type's validation function
|
||||
errs = append(errs, Validate_Max10Type(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("max10ValidatedTypedefPtrField"), obj.Max10ValidatedTypedefPtrField, safe.Field(oldObj, func(oldObj *Struct) *Max10Type { return oldObj.Max10ValidatedTypedefPtrField }), oldObj != nil)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
|
@ -73,18 +73,18 @@ func Test(t *testing.T) {
|
|||
Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 11))),
|
||||
}
|
||||
st.Value(testVal).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.TooLong(field.NewPath("max0Field"), nil, 0),
|
||||
field.TooLong(field.NewPath("max0PtrField"), nil, 0),
|
||||
field.TooLong(field.NewPath("max10Field"), nil, 10),
|
||||
field.TooLong(field.NewPath("max10PtrField"), nil, 10),
|
||||
field.TooLong(field.NewPath("max0UnvalidatedTypedefField"), nil, 0),
|
||||
field.TooLong(field.NewPath("max0UnvalidatedTypedefPtrField"), nil, 0),
|
||||
field.TooLong(field.NewPath("max10UnvalidatedTypedefField"), nil, 10),
|
||||
field.TooLong(field.NewPath("max10UnvalidatedTypedefPtrField"), nil, 10),
|
||||
field.TooLong(field.NewPath("max0ValidatedTypedefField"), nil, 0),
|
||||
field.TooLong(field.NewPath("max0ValidatedTypedefPtrField"), nil, 0),
|
||||
field.TooLong(field.NewPath("max10ValidatedTypedefField"), nil, 10),
|
||||
field.TooLong(field.NewPath("max10ValidatedTypedefPtrField"), nil, 10),
|
||||
field.TooLongCharacters(field.NewPath("max0Field"), "", 0),
|
||||
field.TooLongCharacters(field.NewPath("max0PtrField"), "", 0),
|
||||
field.TooLongCharacters(field.NewPath("max10Field"), "", 10),
|
||||
field.TooLongCharacters(field.NewPath("max10PtrField"), "", 10),
|
||||
field.TooLongCharacters(field.NewPath("max0UnvalidatedTypedefField"), "", 0),
|
||||
field.TooLongCharacters(field.NewPath("max0UnvalidatedTypedefPtrField"), "", 0),
|
||||
field.TooLongCharacters(field.NewPath("max10UnvalidatedTypedefField"), "", 10),
|
||||
field.TooLongCharacters(field.NewPath("max10UnvalidatedTypedefPtrField"), "", 10),
|
||||
field.TooLongCharacters(field.NewPath("max0ValidatedTypedefField"), "", 0),
|
||||
field.TooLongCharacters(field.NewPath("max0ValidatedTypedefPtrField"), "", 0),
|
||||
field.TooLongCharacters(field.NewPath("max10ValidatedTypedefField"), "", 10),
|
||||
field.TooLongCharacters(field.NewPath("max10ValidatedTypedefPtrField"), "", 10),
|
||||
})
|
||||
|
||||
// Test validation ratcheting
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ limitations under the License.
|
|||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"k8s.io/gengo/v2/parser/tags"
|
||||
"k8s.io/gengo/v2/types"
|
||||
)
|
||||
|
|
@ -114,3 +117,23 @@ func IsDirectComparable(t *types.Type) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ParseInt strictly parses an int from a string input,
|
||||
// ensuring that when converted back to a string, the resulting
|
||||
// int and the input string have the exact same representation.
|
||||
// This prevents scenarios where an input like `0100` parses
|
||||
// as 100 and would be re-stringed as `100`.
|
||||
func ParseInt(val string) (int, error) {
|
||||
intVal, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing %q as int: %w", val, err)
|
||||
}
|
||||
|
||||
strVal := strconv.Itoa(intVal)
|
||||
if strVal != val {
|
||||
err := fmt.Errorf("parsed int %d converted to a string value of %q which does not match the input string", intVal, strVal)
|
||||
return 0, fmt.Errorf("parsing %q as int: %w", val, err)
|
||||
}
|
||||
|
||||
return intVal, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -452,3 +452,55 @@ func TestIsDirectComparable(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInt(t *testing.T) {
|
||||
type testcase struct {
|
||||
name string
|
||||
in string
|
||||
expectedOut int
|
||||
expectedError bool
|
||||
}
|
||||
|
||||
testcases := []testcase{
|
||||
{
|
||||
name: "valid canonical integer string",
|
||||
in: "100",
|
||||
expectedOut: 100,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid canonical integer string, not an integer at all",
|
||||
in: "notanint",
|
||||
expectedOut: 0,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid canonical integer string, spurious leading zeros",
|
||||
in: "00100",
|
||||
expectedOut: 0,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid canonical integer string, octal value",
|
||||
in: "0o123",
|
||||
expectedOut: 0,
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, err := ParseInt(tc.in)
|
||||
switch {
|
||||
case tc.expectedError && err == nil:
|
||||
t.Error("expected an error but did not receive one")
|
||||
case !tc.expectedError && err != nil:
|
||||
t.Errorf("received an unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if out != tc.expectedOut {
|
||||
t.Errorf("expected an output value of %d but got %d", tc.expectedOut, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,12 +30,14 @@ const (
|
|||
maxItemsTagName = "k8s:maxItems"
|
||||
minimumTagName = "k8s:minimum"
|
||||
maxLengthTagName = "k8s:maxLength"
|
||||
maxBytesTagName = "k8s:maxBytes"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterTagValidator(maxItemsTagValidator{})
|
||||
RegisterTagValidator(minimumTagValidator{})
|
||||
RegisterTagValidator(maxLengthTagValidator{})
|
||||
RegisterTagValidator(maxBytesTagValidator{})
|
||||
}
|
||||
|
||||
type maxLengthTagValidator struct{}
|
||||
|
|
@ -52,9 +54,7 @@ func (maxLengthTagValidator) ValidScopes() sets.Set[Scope] {
|
|||
return maxLengthTagValidScopes
|
||||
}
|
||||
|
||||
var (
|
||||
maxLengthValidator = types.Name{Package: libValidationPkg, Name: "MaxLength"}
|
||||
)
|
||||
var maxLengthValidator = types.Name{Package: libValidationPkg, Name: "MaxLength"}
|
||||
|
||||
func (maxLengthTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) {
|
||||
var result Validations
|
||||
|
|
@ -65,7 +65,7 @@ func (maxLengthTagValidator) GetValidations(context Context, tag codetags.Tag) (
|
|||
return Validations{}, fmt.Errorf("can only be used on string types (%s)", rootTypeString(context.Type, t))
|
||||
}
|
||||
|
||||
intVal, err := strconv.Atoi(tag.Value)
|
||||
intVal, err := util.ParseInt(tag.Value)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to parse tag payload as int: %w", err)
|
||||
}
|
||||
|
|
@ -80,8 +80,10 @@ func (mltv maxLengthTagValidator) Docs() TagDoc {
|
|||
return TagDoc{
|
||||
Tag: mltv.TagName(),
|
||||
StabilityLevel: TagStabilityLevelBeta,
|
||||
Scopes: mltv.ValidScopes().UnsortedList(),
|
||||
Description: "Indicates that a string field has a limit on its length.",
|
||||
Scopes: sets.List(mltv.ValidScopes()),
|
||||
Description: `Indicates that a string field has a limit on its length in characters.
|
||||
This could allow up to 4*N bytes if multi-byte characters are used.
|
||||
If you want to limit length of bytes specifically, use maxBytes.`,
|
||||
Payloads: []TagPayloadDoc{{
|
||||
Description: "<non-negative integer>",
|
||||
Docs: "This field must be no more than X characters long.",
|
||||
|
|
@ -91,6 +93,59 @@ func (mltv maxLengthTagValidator) Docs() TagDoc {
|
|||
}
|
||||
}
|
||||
|
||||
type maxBytesTagValidator struct{}
|
||||
|
||||
func (maxBytesTagValidator) Init(_ Config) {}
|
||||
|
||||
func (maxBytesTagValidator) TagName() string {
|
||||
return maxBytesTagName
|
||||
}
|
||||
|
||||
var maxBytesTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
|
||||
|
||||
func (maxBytesTagValidator) ValidScopes() sets.Set[Scope] {
|
||||
return maxBytesTagValidScopes
|
||||
}
|
||||
|
||||
var maxBytesValidator = types.Name{Package: libValidationPkg, Name: "MaxBytes"}
|
||||
|
||||
func (maxBytesTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) {
|
||||
var result Validations
|
||||
|
||||
// This tag can apply to value and pointer fields, as well as typedefs
|
||||
// (which should never be pointers). We need to check the concrete type.
|
||||
if t := util.NonPointer(util.NativeType(context.Type)); t != types.String {
|
||||
return Validations{}, fmt.Errorf("can only be used on string types (%s)", rootTypeString(context.Type, t))
|
||||
}
|
||||
|
||||
intVal, err := util.ParseInt(tag.Value)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to parse tag payload as int: %w", err)
|
||||
}
|
||||
if intVal < 0 {
|
||||
return result, fmt.Errorf("must be greater than or equal to zero")
|
||||
}
|
||||
result.AddFunction(Function(maxBytesTagName, DefaultFlags, maxBytesValidator, intVal))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (mltv maxBytesTagValidator) Docs() TagDoc {
|
||||
return TagDoc{
|
||||
Tag: mltv.TagName(),
|
||||
StabilityLevel: TagStabilityLevelBeta,
|
||||
Scopes: sets.List(mltv.ValidScopes()),
|
||||
Description: `Indicates that a string field has a limit on its length in bytes.
|
||||
This could only allow as few as N/4 multi-byte characters.
|
||||
If you want to limit length of characters specifically, use maxLength.`,
|
||||
Payloads: []TagPayloadDoc{{
|
||||
Description: "<non-negative integer>",
|
||||
Docs: "This field must be no more than X bytes long.",
|
||||
}},
|
||||
PayloadsType: codetags.ValueTypeInt,
|
||||
PayloadsRequired: true,
|
||||
}
|
||||
}
|
||||
|
||||
type maxItemsTagValidator struct{}
|
||||
|
||||
func (maxItemsTagValidator) Init(_ Config) {}
|
||||
|
|
@ -110,9 +165,7 @@ func (maxItemsTagValidator) ValidScopes() sets.Set[Scope] {
|
|||
return maxItemsTagValidScopes
|
||||
}
|
||||
|
||||
var (
|
||||
maxItemsValidator = types.Name{Package: libValidationPkg, Name: "MaxItems"}
|
||||
)
|
||||
var maxItemsValidator = types.Name{Package: libValidationPkg, Name: "MaxItems"}
|
||||
|
||||
func (maxItemsTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) {
|
||||
var result Validations
|
||||
|
|
@ -163,9 +216,7 @@ func (minimumTagValidator) ValidScopes() sets.Set[Scope] {
|
|||
return minimumTagValidScopes
|
||||
}
|
||||
|
||||
var (
|
||||
minimumValidator = types.Name{Package: libValidationPkg, Name: "Minimum"}
|
||||
)
|
||||
var minimumValidator = types.Name{Package: libValidationPkg, Name: "Minimum"}
|
||||
|
||||
func (minimumTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) {
|
||||
var result Validations
|
||||
|
|
|
|||
Loading…
Reference in a new issue