Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ require (
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
gopkg.in/evanphx/json-patch.v4 v4.13.0
gopkg.in/go-jose/go-jose.v2 v2.6.3
gopkg.in/inf.v0 v0.9.1
k8s.io/api v0.0.0
k8s.io/apiextensions-apiserver v0.0.0
k8s.io/apimachinery v0.0.0
Expand Down Expand Up @@ -211,7 +212,6 @@ require (
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/mod v0.35.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/gengo/v2 v2.0.0-20260408192533-25e2208e0dc3 // indirect
Expand Down
20 changes: 15 additions & 5 deletions pkg/apis/resource/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (
"strconv"
"strings"

infdec "gopkg.in/inf.v0"

"github.com/google/uuid"

corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -1194,11 +1196,19 @@ func validateRequestPolicyRange(defaultValue apiresource.Quantity, maxCapacity a

func validateRequestPolicyRangeStep(value, min, step apiresource.Quantity, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
stepVal := step.Value()
minVal := min.Value()
val := value.Value()
added := (val - minVal)
if added%stepVal != 0 {
// span = value - min; check span is a whole-number multiple of step.
// Using inf.Dec arithmetic avoids int64 overflow and is correct at any scale.
// DeepCopy before AsDec to avoid mutating the originals.
span := value.DeepCopy()
span.Sub(min)
spanDec := span.AsDec()
stepCopy := step.DeepCopy()
stepDec := stepCopy.AsDec()
// n = floor(span / step); if n*step != span then it's not aligned.
var nDec, remDec, mulDec infdec.Dec
n := nDec.QuoRound(spanDec, stepDec, 0, infdec.RoundDown)
remainder := remDec.Sub(spanDec, mulDec.Mul(n, stepDec))
if remainder.Sign() != 0 {
allErrs = append(allErrs, field.Invalid(fldPath, value.String(), fmt.Sprintf("value is not a multiple of a given step (%s) from (%s)", step.String(), min.String())))
}
return allErrs
Expand Down
33 changes: 33 additions & 0 deletions pkg/apis/resource/validation/validation_device_capacity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,39 @@ func TestValidateDeviceCapacity(t *testing.T) {
field.Invalid(validRangeField.Child("step"), "10Gi", "value is not a multiple of a given step (2Gi) from (1Gi)"),
},
},
"valid-range-fractional-step": {
// min=0.2, step=0.1, max=1, default=0.2, capacity=1: 0.2 = min+0*0.1, 1.0 = min+8*0.1
capacity: testDeviceCapacity(
apiresource.MustParse("1"),
testCapacityRequestPolicy(
ptr.To(apiresource.MustParse("200m")),
nil,
testValidRange(
ptr.To(apiresource.MustParse("200m")),
ptr.To(apiresource.MustParse("1")),
ptr.To(apiresource.MustParse("100m")),
),
),
),
},
"invalid-range-fractional-step-not-aligned": {
// default=0.25 is not a multiple of 0.1 from 0.2
capacity: testDeviceCapacity(
apiresource.MustParse("1"),
testCapacityRequestPolicy(
ptr.To(apiresource.MustParse("250m")),
nil,
testValidRange(
ptr.To(apiresource.MustParse("200m")),
ptr.To(apiresource.MustParse("1")),
ptr.To(apiresource.MustParse("100m")),
),
),
),
wantFailures: field.ErrorList{
field.Invalid(validRangeField.Child("step"), "250m", "value is not a multiple of a given step (100m) from (200m)"),
},
},
"invalid-range-large-step": {
capacity: testDeviceCapacity(maxCapacity, testCapacityRequestPolicy(&one, nil, testValidRange(ptr.To(one), nil, ptr.To(maxCapacity)))),
wantFailures: field.ErrorList{
Expand Down
2 changes: 1 addition & 1 deletion staging/src/k8s.io/dynamic-resource-allocation/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/stretchr/testify v1.11.1
go.etcd.io/etcd/client/pkg/v3 v3.7.0-rc.0
google.golang.org/grpc v1.81.1
gopkg.in/inf.v0 v0.9.1
k8s.io/api v0.0.0
k8s.io/apimachinery v0.0.0
k8s.io/apiserver v0.0.0
Expand Down Expand Up @@ -82,7 +83,6 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/kube-openapi v0.0.0-20260618221249-bc653b64f974 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package experimental
import (
"errors"

inf "gopkg.in/inf.v0"
resourceapi "k8s.io/api/resource/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/utils/ptr"
Expand Down Expand Up @@ -118,17 +119,30 @@ func roundUpRange(requestedVal *resource.Quantity, validRange *resourceapi.Capac
if validRange.Step == nil {
return *requestedVal
}
requestedInt64 := requestedVal.Value()
step := validRange.Step.Value()
min := validRange.Min.Value()
added := (requestedInt64 - min)
n := added / step
mod := added % step
if mod != 0 {
n += 1
}
val := min + step*n
return *resource.NewQuantity(val, resource.BinarySI)
// Compute how far requested is above min, then round up to the next
// multiple of step. All arithmetic uses inf.Dec via Quantity.AsDec() so
// it is exact at any scale and cannot overflow.
span := requestedVal.DeepCopy()
span.Sub(*validRange.Min) // span = requested - min
stepCopy := validRange.Step.DeepCopy()
stepDec := stepCopy.AsDec()
minCopy := validRange.Min.DeepCopy()
minDec := minCopy.AsDec()

// n = ceil(span / step)
var nDec, resultDec, mulDec inf.Dec
n := nDec.QuoRound(span.AsDec(), stepDec, 0, inf.RoundUp)

// result = min + n*step, computed entirely in Dec.
resultDec.Add(minDec, mulDec.Mul(n, stepDec))

// If the result is a whole number, use NewQuantity to keep the representation compact
// and compatible with quantities parsed from whole-number strings.
format := validRange.Step.Format
if v, ok := resultDec.Unscaled(); ok && resultDec.Scale() == 0 {
return *resource.NewQuantity(v, format)
}
return *resource.NewDecimalQuantity(resultDec, format)
}

// roundUpValidValues returns the first value in validValues that is greater than or equal to requestedVal.
Expand Down Expand Up @@ -188,13 +202,18 @@ func violateValidRange(requestedVal resource.Quantity, validRange resourceapi.Ca
return true
}
if validRange.Step != nil {
requestedInt64 := requestedVal.Value()
step := validRange.Step.Value()
min := validRange.Min.Value()
added := (requestedInt64 - min)
mod := added % step
// must be a multiply of step
if mod != 0 {
// span = requested - min; check span is a whole-number multiple of step.
// DeepCopy before AsDec to avoid mutating the originals.
span := requestedVal.DeepCopy()
span.Sub(*validRange.Min)
spanDec := span.AsDec()
stepCopy := validRange.Step.DeepCopy()
stepDec := stepCopy.AsDec()
// n = floor(span / step); if n*step != span then it's not aligned.
var nDec, remDec, mulDec inf.Dec
n := nDec.QuoRound(spanDec, stepDec, 0, inf.RoundDown)
remainder := remDec.Sub(spanDec, mulDec.Mul(n, stepDec))
if remainder.Sign() != 0 {
return true
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ var (
one = resource.MustParse("1")
two = resource.MustParse("2")
three = resource.MustParse("3")

pointTwoFive = resource.MustParse("250m")
pointFour = resource.MustParse("400m")
pointThree = resource.MustParse("300m")
pointTwo = resource.MustParse("200m")
pointOne = resource.MustParse("100m")
)

func deviceConsumedCapacity(deviceID DeviceID) DeviceConsumedCapacity {
Expand Down Expand Up @@ -155,6 +161,31 @@ func testViolateCapacityRequestPolicy(t *testing.T) {
},
true,
},
// fractional step: min=0.2, step=0.1, max=1
"fractional step aligned (0.3 = min+1*step)": {
pointThree,
&resourceapi.CapacityRequestPolicy{
Default: &pointTwo,
ValidRange: &resourceapi.CapacityRequestPolicyRange{
Min: &pointTwo,
Max: &one,
Step: &pointOne,
},
},
false,
},
"fractional step not aligned (0.25 is not a multiple of 0.1 from 0.2)": {
pointTwoFive,
&resourceapi.CapacityRequestPolicy{
Default: &pointTwo,
ValidRange: &resourceapi.CapacityRequestPolicyRange{
Min: &pointTwo,
Max: &one,
Step: &pointOne,
},
},
true,
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
Expand Down Expand Up @@ -210,6 +241,34 @@ func testCalculateConsumedCapacity(t *testing.T) {
&resourceapi.CapacityRequestPolicy{Default: ptr.To(one), ValidRange: &resourceapi.CapacityRequestPolicyRange{Min: ptr.To(one), Step: ptr.To(one.DeepCopy())}},
two,
},
// fractional step: min=0.2, step=0.1, max=1; request=0.25; rounds up to 0.3
"fractional step round up (0.25 to 0.3)": {
&pointTwoFive,
resource.MustParse("1"),
&resourceapi.CapacityRequestPolicy{
Default: &pointTwo,
ValidRange: &resourceapi.CapacityRequestPolicyRange{
Min: &pointTwo,
Max: &one,
Step: &pointOne,
},
},
resource.MustParse("300m"),
},
// fractional step: request already aligned; no rounding
"fractional step already aligned (0.4 = min+2*step)": {
&pointFour,
resource.MustParse("1"),
&resourceapi.CapacityRequestPolicy{
Default: &pointTwo,
ValidRange: &resourceapi.CapacityRequestPolicyRange{
Min: &pointTwo,
Max: &one,
Step: &pointOne,
},
},
resource.MustParse("400m"),
},
"valid value in set": {
&two,
three,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package incubating
import (
"errors"

inf "gopkg.in/inf.v0"
resourceapi "k8s.io/api/resource/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/utils/ptr"
Expand Down Expand Up @@ -118,17 +119,30 @@ func roundUpRange(requestedVal *resource.Quantity, validRange *resourceapi.Capac
if validRange.Step == nil {
return *requestedVal
}
requestedInt64 := requestedVal.Value()
step := validRange.Step.Value()
min := validRange.Min.Value()
added := (requestedInt64 - min)
n := added / step
mod := added % step
if mod != 0 {
n += 1
}
val := min + step*n
return *resource.NewQuantity(val, resource.BinarySI)
// Compute how far requested is above min, then round up to the next
// multiple of step. All arithmetic uses inf.Dec via Quantity.AsDec() so
// it is exact at any scale and cannot overflow.
span := requestedVal.DeepCopy()
span.Sub(*validRange.Min) // span = requested - min
stepCopy := validRange.Step.DeepCopy()
stepDec := stepCopy.AsDec()
minCopy := validRange.Min.DeepCopy()
minDec := minCopy.AsDec()

// n = ceil(span / step)
var nDec, resultDec, mulDec inf.Dec
n := nDec.QuoRound(span.AsDec(), stepDec, 0, inf.RoundUp)

// result = min + n*step, computed entirely in Dec.
resultDec.Add(minDec, mulDec.Mul(n, stepDec))

// If the result is a whole number, use NewQuantity to keep the representation compact
// and compatible with quantities parsed from whole-number strings.
format := validRange.Step.Format
if v, ok := resultDec.Unscaled(); ok && resultDec.Scale() == 0 {
return *resource.NewQuantity(v, format)
}
return *resource.NewDecimalQuantity(resultDec, format)
}

// roundUpValidValues returns the first value in validValues that is greater than or equal to requestedVal.
Expand Down Expand Up @@ -188,13 +202,18 @@ func violateValidRange(requestedVal resource.Quantity, validRange resourceapi.Ca
return true
}
if validRange.Step != nil {
requestedInt64 := requestedVal.Value()
step := validRange.Step.Value()
min := validRange.Min.Value()
added := (requestedInt64 - min)
mod := added % step
// must be a multiply of step
if mod != 0 {
// span = requested - min; check span is a whole-number multiple of step.
// DeepCopy before AsDec to avoid mutating the originals.
span := requestedVal.DeepCopy()
span.Sub(*validRange.Min)
spanDec := span.AsDec()
stepCopy := validRange.Step.DeepCopy()
stepDec := stepCopy.AsDec()
// n = floor(span / step); if n*step != span then it's not aligned.
var nDec, remDec, mulDec inf.Dec
n := nDec.QuoRound(spanDec, stepDec, 0, inf.RoundDown)
remainder := remDec.Sub(spanDec, mulDec.Mul(n, stepDec))
if remainder.Sign() != 0 {
return true
}
}
Expand Down
Loading