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:
Kubernetes Prow Robot 2026-03-04 05:20:27 +05:30 committed by GitHub
commit e08e598df0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 957 additions and 93 deletions

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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)...)

View file

@ -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)...)

View file

@ -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)...)

View file

@ -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,

View file

@ -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')"),

View file

@ -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: "",
},

View file

@ -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": {

View file

@ -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;
}

View file

@ -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"`
}

View file

@ -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 {

View file

@ -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;
}

View file

@ -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"`
}

View file

@ -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 {

View file

@ -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;
}

View file

@ -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"`
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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)
})
}
}

View file

@ -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 {

View file

@ -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"`
}

View file

@ -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"`
}

View file

@ -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"`
}

View file

@ -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

View file

@ -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()
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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)
}
})
}
}

View file

@ -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