diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index d347a20bb5c..1effbd3ea8b 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -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": { diff --git a/api/openapi-spec/v3/apis__resource.k8s.io__v1_openapi.json b/api/openapi-spec/v3/apis__resource.k8s.io__v1_openapi.json index 340cc494cea..bbbdbdc8a46 100644 --- a/api/openapi-spec/v3/apis__resource.k8s.io__v1_openapi.json +++ b/api/openapi-spec/v3/apis__resource.k8s.io__v1_openapi.json @@ -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": { diff --git a/api/openapi-spec/v3/apis__resource.k8s.io__v1beta1_openapi.json b/api/openapi-spec/v3/apis__resource.k8s.io__v1beta1_openapi.json index b7539df1689..894bed9e1c0 100644 --- a/api/openapi-spec/v3/apis__resource.k8s.io__v1beta1_openapi.json +++ b/api/openapi-spec/v3/apis__resource.k8s.io__v1beta1_openapi.json @@ -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": { diff --git a/api/openapi-spec/v3/apis__resource.k8s.io__v1beta2_openapi.json b/api/openapi-spec/v3/apis__resource.k8s.io__v1beta2_openapi.json index 2104fbae587..dad6c5644da 100644 --- a/api/openapi-spec/v3/apis__resource.k8s.io__v1beta2_openapi.json +++ b/api/openapi-spec/v3/apis__resource.k8s.io__v1beta2_openapi.json @@ -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": { diff --git a/pkg/apis/resource/v1/zz_generated.validations.go b/pkg/apis/resource/v1/zz_generated.validations.go index a46ece5c3b3..78860ec8aea 100644 --- a/pkg/apis/resource/v1/zz_generated.validations.go +++ b/pkg/apis/resource/v1/zz_generated.validations.go @@ -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)...) diff --git a/pkg/apis/resource/v1beta1/zz_generated.validations.go b/pkg/apis/resource/v1beta1/zz_generated.validations.go index 1699f58670c..dfdfafb30e7 100644 --- a/pkg/apis/resource/v1beta1/zz_generated.validations.go +++ b/pkg/apis/resource/v1beta1/zz_generated.validations.go @@ -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)...) diff --git a/pkg/apis/resource/v1beta2/zz_generated.validations.go b/pkg/apis/resource/v1beta2/zz_generated.validations.go index 0939e739459..934c36458e9 100644 --- a/pkg/apis/resource/v1beta2/zz_generated.validations.go +++ b/pkg/apis/resource/v1beta2/zz_generated.validations.go @@ -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)...) diff --git a/pkg/apis/resource/validation/validation.go b/pkg/apis/resource/validation/validation.go index b2847e98980..a3ad1513705 100644 --- a/pkg/apis/resource/validation/validation.go +++ b/pkg/apis/resource/validation/validation.go @@ -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, diff --git a/pkg/apis/resource/validation/validation_resourceclaim_test.go b/pkg/apis/resource/validation/validation_resourceclaim_test.go index d3347754cba..be49d6aed6b 100644 --- a/pkg/apis/resource/validation/validation_resourceclaim_test.go +++ b/pkg/apis/resource/validation/validation_resourceclaim_test.go @@ -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"), "", "error parsing data as JSON: invalid character 'o' in literal false (expecting 'a')"), diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index f2c1bee80a4..c93e5ff7c37 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -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: "", }, diff --git a/pkg/registry/resource/resourceclaim/declarative_validation_test.go b/pkg/registry/resource/resourceclaim/declarative_validation_test.go index bc1a7ce4e7c..745113925e3 100644 --- a/pkg/registry/resource/resourceclaim/declarative_validation_test.go +++ b/pkg/registry/resource/resourceclaim/declarative_validation_test.go @@ -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": { diff --git a/staging/src/k8s.io/api/resource/v1/generated.proto b/staging/src/k8s.io/api/resource/v1/generated.proto index 280b31ec439..ebb88b6f045 100644 --- a/staging/src/k8s.io/api/resource/v1/generated.proto +++ b/staging/src/k8s.io/api/resource/v1/generated.proto @@ -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; } diff --git a/staging/src/k8s.io/api/resource/v1/types.go b/staging/src/k8s.io/api/resource/v1/types.go index 70d32521f92..15ec787346c 100644 --- a/staging/src/k8s.io/api/resource/v1/types.go +++ b/staging/src/k8s.io/api/resource/v1/types.go @@ -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"` } diff --git a/staging/src/k8s.io/api/resource/v1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/resource/v1/types_swagger_doc_generated.go index 2a78b62d8b0..a3aa940e598 100644 --- a/staging/src/k8s.io/api/resource/v1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/resource/v1/types_swagger_doc_generated.go @@ -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 { diff --git a/staging/src/k8s.io/api/resource/v1beta1/generated.proto b/staging/src/k8s.io/api/resource/v1beta1/generated.proto index 3f997b88a14..14cec7635c0 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/resource/v1beta1/generated.proto @@ -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; } diff --git a/staging/src/k8s.io/api/resource/v1beta1/types.go b/staging/src/k8s.io/api/resource/v1beta1/types.go index 9f71a226976..1242db2d1e7 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/types.go +++ b/staging/src/k8s.io/api/resource/v1beta1/types.go @@ -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"` } diff --git a/staging/src/k8s.io/api/resource/v1beta1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/resource/v1beta1/types_swagger_doc_generated.go index 4c3138fd1ea..ad81bc05520 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/resource/v1beta1/types_swagger_doc_generated.go @@ -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 { diff --git a/staging/src/k8s.io/api/resource/v1beta2/generated.proto b/staging/src/k8s.io/api/resource/v1beta2/generated.proto index 5d5076ecf2b..4fec76e2a63 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/generated.proto +++ b/staging/src/k8s.io/api/resource/v1beta2/generated.proto @@ -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; } diff --git a/staging/src/k8s.io/api/resource/v1beta2/types.go b/staging/src/k8s.io/api/resource/v1beta2/types.go index 288b30d136f..e4979efde0a 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/types.go +++ b/staging/src/k8s.io/api/resource/v1beta2/types.go @@ -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"` } diff --git a/staging/src/k8s.io/api/resource/v1beta2/types_swagger_doc_generated.go b/staging/src/k8s.io/api/resource/v1beta2/types_swagger_doc_generated.go index d7640e608d2..986fbb3837f 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/resource/v1beta2/types_swagger_doc_generated.go @@ -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 { diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go index b6db5e08cbe..ce91a2a8624 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go @@ -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 } diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go index f17937c6913..44e866ad8db 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go @@ -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) + }) + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go b/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go index b90370c0c8e..f1f227acf73 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go @@ -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: "", + Detail: msg, + } +} + // TooLongMaxLength returns a *Error indicating "too long". // Deprecated: Use TooLong instead. func TooLongMaxLength(field *Path, value interface{}, maxLength int) *Error { diff --git a/staging/src/k8s.io/client-go/applyconfigurations/resource/v1/networkdevicedata.go b/staging/src/k8s.io/client-go/applyconfigurations/resource/v1/networkdevicedata.go index 5d0ee8fc501..9cb1b4dea5b 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/resource/v1/networkdevicedata.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/resource/v1/networkdevicedata.go @@ -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"` } diff --git a/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta1/networkdevicedata.go b/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta1/networkdevicedata.go index 5c3edcfe886..f112f01454c 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta1/networkdevicedata.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta1/networkdevicedata.go @@ -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"` } diff --git a/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta2/networkdevicedata.go b/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta2/networkdevicedata.go index 9621703268f..238e5412f13 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta2/networkdevicedata.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta2/networkdevicedata.go @@ -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"` } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc.go new file mode 100644 index 00000000000..79873c95df8 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc.go @@ -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 diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc_test.go new file mode 100644 index 00000000000..47bc3f416d9 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc_test.go @@ -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() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/zz_generated.validations.go new file mode 100644 index 00000000000..4f93d9945a8 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/zz_generated.validations.go @@ -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 +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxlength/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxlength/doc_test.go index 6524a471425..88b489db08f 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxlength/doc_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxlength/doc_test.go @@ -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 diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util.go index 4566699baff..208a5ea19b0 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util.go @@ -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 +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util_test.go index 5ff3ae6cb8c..554e7bc3a80 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util_test.go @@ -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) + } + }) + } +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go index 70fd36a52a7..573c266307e 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go @@ -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: "", 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: "", + 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