From ba9ce984908a37a77d021815a6b6d30907a13b2a Mon Sep 17 00:00:00 2001 From: tylerslaton Date: Thu, 1 Feb 2024 18:44:09 -0500 Subject: [PATCH] enhancement: add ComputeClasses and VolumeClasses fields to BaseResources This commit enhances the BaseResources struct by adding ComputeClasses and VolumeClasses fields. These new fields allow for a more detailed specification of compute and storage resources, categorized by class instead of just by resource type. This enhancement is crucial for accurately tracking the usage of memory, CPU, and storage for each specific compute or volume class. Consequently, the QuotaRequest logic has been updated to account for these new fields. These new fields are maps and they introduce a unique approach to handle unlimited resources. They include special keys, `AllComputeClasses` and `AllVolumeClasses`. If a value is assigned to these keys in ComputeClassResources or VolumeClassResources, all compute or volume classes will be evaluated against the value. Signed-off-by: tylerslaton --- .../v1/baseresources.go | 87 ++-- .../v1/baseresources_test.go | 453 +++++++++-------- .../v1/computeclassresources.go | 150 ++++++ .../v1/computeclassresources_test.go | 467 ++++++++++++++++++ .../v1/quotarequests.go | 3 +- .../v1/quotarequests_test.go | 305 ++++++++---- .../internal.admin.acorn.io/v1/resources.go | 25 +- .../v1/volumeclassresources.go | 119 +++++ .../v1/volumeclassresources_test.go | 302 +++++++++++ .../v1/zz_generated.deepcopy.go | 92 +++- pkg/controller/quota/quota.go | 17 +- .../quota/testdata/basic/expected.golden | 15 +- .../quota/testdata/basic/input.yaml | 12 +- .../testdata/implicit-pv-bind/expected.golden | 13 +- .../expected.golden | 15 +- pkg/openapi/generated/openapi_generated.go | 118 ++++- 16 files changed, 1788 insertions(+), 405 deletions(-) create mode 100644 pkg/apis/internal.admin.acorn.io/v1/computeclassresources.go create mode 100644 pkg/apis/internal.admin.acorn.io/v1/computeclassresources_test.go create mode 100644 pkg/apis/internal.admin.acorn.io/v1/volumeclassresources.go create mode 100644 pkg/apis/internal.admin.acorn.io/v1/volumeclassresources_test.go diff --git a/pkg/apis/internal.admin.acorn.io/v1/baseresources.go b/pkg/apis/internal.admin.acorn.io/v1/baseresources.go index 58c66ebab..f5db33062 100644 --- a/pkg/apis/internal.admin.acorn.io/v1/baseresources.go +++ b/pkg/apis/internal.admin.acorn.io/v1/baseresources.go @@ -1,10 +1,9 @@ package v1 import ( + "errors" "fmt" "strings" - - "k8s.io/apimachinery/pkg/api/resource" ) // BaseResources defines resources that should be tracked at any scoped. The two main exclusions @@ -16,9 +15,10 @@ type BaseResources struct { Volumes int `json:"volumes"` Images int `json:"images"` - VolumeStorage resource.Quantity `json:"volumeStorage"` - Memory resource.Quantity `json:"memory"` - CPU resource.Quantity `json:"cpu"` + // ComputeClasses and VolumeClasses are used to track the amount of compute and volume storage per their + // respective classes + ComputeClasses ComputeClassResources `json:"computeClasses"` + VolumeClasses VolumeClassResources `json:"volumeClasses"` } // Add will add the BaseResources of another BaseResources struct into the current one. @@ -29,9 +29,14 @@ func (current *BaseResources) Add(incoming BaseResources) { current.Volumes = Add(current.Volumes, incoming.Volumes) current.Images = Add(current.Images, incoming.Images) - current.VolumeStorage = AddQuantity(current.VolumeStorage, incoming.VolumeStorage) - current.Memory = AddQuantity(current.Memory, incoming.Memory) - current.CPU = AddQuantity(current.CPU, incoming.CPU) + if current.ComputeClasses == nil { + current.ComputeClasses = ComputeClassResources{} + } + if current.VolumeClasses == nil { + current.VolumeClasses = VolumeClassResources{} + } + current.ComputeClasses.Add(incoming.ComputeClasses) + current.VolumeClasses.Add(incoming.VolumeClasses) } // Remove will remove the BaseResources of another BaseResources struct from the current one. Calling remove @@ -42,13 +47,9 @@ func (current *BaseResources) Remove(incoming BaseResources, all bool) { current.Jobs = Sub(current.Jobs, incoming.Jobs) current.Volumes = Sub(current.Volumes, incoming.Volumes) current.Images = Sub(current.Images, incoming.Images) - - current.Memory = SubQuantity(current.Memory, incoming.Memory) - current.CPU = SubQuantity(current.CPU, incoming.CPU) - - // Only remove persistent resources if all is true. + current.ComputeClasses.Remove(incoming.ComputeClasses) if all { - current.VolumeStorage = SubQuantity(current.VolumeStorage, incoming.VolumeStorage) + current.VolumeClasses.Remove(incoming.VolumeClasses) } } @@ -58,6 +59,7 @@ func (current *BaseResources) Remove(incoming BaseResources, all bool) { // If the current BaseResources defines unlimited, then it will always fit. func (current *BaseResources) Fits(incoming BaseResources) error { var exceededResources []string + var errs []error // Check if any of the resources are exceeded for _, r := range []struct { @@ -75,31 +77,26 @@ func (current *BaseResources) Fits(incoming BaseResources) error { } } - // Check if any of the quantity resources are exceeded - for _, r := range []struct { - resource string - current, incoming resource.Quantity - }{ - {"VolumeStorage", current.VolumeStorage, incoming.VolumeStorage}, - {"Memory", current.Memory, incoming.Memory}, - {"Cpu", current.CPU, incoming.CPU}, - } { - if !FitsQuantity(r.current, r.incoming) { - exceededResources = append(exceededResources, r.resource) - } + if len(exceededResources) != 0 { + errs = append(errs, fmt.Errorf("%w: %s", ErrExceededResources, strings.Join(exceededResources, ", "))) } - // Build an aggregated error message for the exceeded resources - if len(exceededResources) > 0 { - return fmt.Errorf("%w: %s", ErrExceededResources, strings.Join(exceededResources, ", ")) + if err := current.ComputeClasses.Fits(incoming.ComputeClasses); err != nil { + errs = append(errs, err) + } + + if err := current.VolumeClasses.Fits(incoming.VolumeClasses); err != nil { + errs = append(errs, err) } - return nil + // Build an aggregated error message for the exceeded resources + return errors.Join(errs...) } // ToString will return a string representation of the BaseResources within the struct. func (current *BaseResources) ToString() string { - return ResourcesToString( + // make sure that an empty string doesn't have a comma + result := CountResourcesToString( map[string]int{ "Apps": current.Apps, "Containers": current.Containers, @@ -107,11 +104,24 @@ func (current *BaseResources) ToString() string { "Volumes": current.Volumes, "Images": current.Images, }, - map[string]resource.Quantity{ - "VolumeStorage": current.VolumeStorage, - "Memory": current.Memory, - "Cpu": current.CPU, - }) + ) + + for _, resource := range []struct { + name string + asString string + }{ + {"ComputeClasses", current.ComputeClasses.ToString()}, + {"VolumeClasses", current.VolumeClasses.ToString()}, + } { + if result != "" && resource.asString != "" { + result += ", " + } + if resource.asString != "" { + result += fmt.Sprintf("%s: %s", resource.name, resource.asString) + } + } + + return result } // Equals will check if the current BaseResources struct is equal to another. This is useful @@ -122,7 +132,6 @@ func (current *BaseResources) Equals(incoming BaseResources) bool { current.Jobs == incoming.Jobs && current.Volumes == incoming.Volumes && current.Images == incoming.Images && - current.VolumeStorage.Cmp(incoming.VolumeStorage) == 0 && - current.Memory.Cmp(incoming.Memory) == 0 && - current.CPU.Cmp(incoming.CPU) == 0 + current.ComputeClasses.Equals(incoming.ComputeClasses) && + current.VolumeClasses.Equals(incoming.VolumeClasses) } diff --git a/pkg/apis/internal.admin.acorn.io/v1/baseresources_test.go b/pkg/apis/internal.admin.acorn.io/v1/baseresources_test.go index 27a7b1ae7..4c1432edb 100644 --- a/pkg/apis/internal.admin.acorn.io/v1/baseresources_test.go +++ b/pkg/apis/internal.admin.acorn.io/v1/baseresources_test.go @@ -17,79 +17,86 @@ func TestBaseResourcesAdd(t *testing.T) { expected BaseResources }{ { - name: "add to empty BaseResources resources", - current: BaseResources{}, - incoming: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), - }, - expected: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), - }, + name: "add to empty BaseResources resources", + current: BaseResources{}, + incoming: BaseResources{Apps: 1}, + expected: BaseResources{Apps: 1}, }, { - name: "add to existing BaseResources resources", - current: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), - }, + name: "add to existing BaseResources resources", + current: BaseResources{Apps: 1}, incoming: BaseResources{ - Apps: 1, - Images: 1, - VolumeStorage: resource.MustParse("1Mi"), - CPU: resource.MustParse("20m"), + Apps: 1, + Images: 1, }, expected: BaseResources{ - Apps: 2, - Images: 1, - VolumeStorage: resource.MustParse("2Mi"), - CPU: resource.MustParse("20m"), + Apps: 2, + Images: 1, }, }, { - name: "add where current has a resource specified with unlimited", - current: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), - }, - incoming: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), - }, - expected: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), - }, + name: "add where current has a resource specified with unlimited", + current: BaseResources{Apps: Unlimited}, + incoming: BaseResources{Apps: 1}, + expected: BaseResources{Apps: Unlimited}, + }, + { + name: "add where incoming has a resource specified with unlimited", + current: BaseResources{Apps: 1}, + incoming: BaseResources{Apps: Unlimited}, + expected: BaseResources{Apps: Unlimited}, + }, + { + name: "add where current and incoming have a resource specified with unlimited", + current: BaseResources{Apps: Unlimited}, + incoming: BaseResources{Apps: Unlimited}, + expected: BaseResources{Apps: Unlimited}, }, { - name: "add where incoming has a resource specified with unlimited", + name: "add where current and incoming have ComputeClasses and VolumeClasses", current: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, incoming: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, expected: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: 2, Containers: 2, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("2Mi"), + CPU: resource.MustParse("2m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("2Mi")}}, }, }, { - name: "add where current and incoming have a resource specified with unlimited", - current: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), - }, + name: "add where current is empty and incoming has ComputeClasses and VolumeClasses", + current: BaseResources{}, incoming: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, expected: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, }, } @@ -113,107 +120,115 @@ func TestBaseResourcesRemove(t *testing.T) { expected BaseResources }{ { - name: "remove from empty BaseResources resources", - current: BaseResources{}, - incoming: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), - }, + name: "remove from empty BaseResources resources", + current: BaseResources{}, + incoming: BaseResources{Apps: 1}, expected: BaseResources{}, }, { - name: "remove from existing BaseResources resources", - current: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), - }, - incoming: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), - }, + name: "remove from existing BaseResources resources", + current: BaseResources{Apps: 1}, + incoming: BaseResources{Apps: 1}, expected: BaseResources{}, }, { - name: "should never get negative values", - all: true, - current: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), - VolumeStorage: resource.MustParse("1Mi"), - }, - incoming: BaseResources{ - Apps: 2, - Memory: resource.MustParse("2Mi"), - VolumeStorage: resource.MustParse("2Mi"), - }, + name: "should never get negative values", + all: true, + current: BaseResources{Apps: 1}, + incoming: BaseResources{Apps: 2}, expected: BaseResources{}, }, { - name: "remove persistent resources with all", - current: BaseResources{ - VolumeStorage: resource.MustParse("1Mi"), - }, - incoming: BaseResources{ - VolumeStorage: resource.MustParse("1Mi"), - }, - all: true, - expected: BaseResources{}, + name: "remove where current has a resource specified with unlimited", + current: BaseResources{Apps: Unlimited}, + incoming: BaseResources{Apps: 1}, + expected: BaseResources{Apps: Unlimited}, }, { - name: "does not remove persistent resources without all", - current: BaseResources{ - VolumeStorage: resource.MustParse("1Mi"), - }, - incoming: BaseResources{ - VolumeStorage: resource.MustParse("1Mi"), - }, - expected: BaseResources{ - VolumeStorage: resource.MustParse("1Mi"), - }, + name: "remove where incoming has a resource specified with unlimited", + current: BaseResources{Apps: 1}, + incoming: BaseResources{Apps: Unlimited}, + expected: BaseResources{Apps: 1}, }, { - name: "remove where current has a resource specified with unlimited", + name: "remove where current and incoming have a resource specified with unlimited", + current: BaseResources{Apps: Unlimited}, + incoming: BaseResources{Apps: Unlimited}, + expected: BaseResources{Apps: Unlimited}, + }, + { + name: "remove where current and incoming have ComputeClasses and VolumeClasses", current: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, incoming: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), - }, - expected: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, + all: true, + expected: BaseResources{}, }, { - name: "remove where incoming has a resource specified with unlimited", + name: "does not remove volume storage when all is false", + expected: BaseResources{ + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + }, current: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, incoming: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), - }, - expected: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, }, { - name: "remove where current and incoming have a resource specified with unlimited", + name: "remove where current has two ComputeClasses and VolumeClasses and incoming has one", current: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + ComputeClasses: ComputeClassResources{ + "foo": { + Memory: resource.MustParse("2Mi"), + CPU: resource.MustParse("2m"), + }, + "bar": { + Memory: resource.MustParse("2Mi"), + CPU: resource.MustParse("2m"), + }, + }, + VolumeClasses: VolumeClassResources{ + "foo": {resource.MustParse("2Mi")}, + "bar": {resource.MustParse("2Mi")}, + }, }, incoming: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), - }, + ComputeClasses: ComputeClassResources{ + "foo": { + Memory: resource.MustParse("2Mi"), + CPU: resource.MustParse("2m"), + }, + }, + VolumeClasses: VolumeClassResources{ + "foo": {resource.MustParse("2Mi")}, + }, + }, + all: true, expected: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + ComputeClasses: ComputeClassResources{ + "bar": { + Memory: resource.MustParse("2Mi"), + CPU: resource.MustParse("2m"), + }, + }, + VolumeClasses: VolumeClassResources{ + "bar": {resource.MustParse("2Mi")}, + }, }, }, } @@ -242,40 +257,62 @@ func TestBaseResourcesEquals(t *testing.T) { expected: true, }, { - name: "equal BaseResources resources", - current: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), - }, - incoming: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), - }, + name: "equal BaseResources resources", + current: BaseResources{Apps: 1}, + incoming: BaseResources{Apps: 1}, expected: true, }, { - name: "unequal BaseResources resources", + name: "unequal BaseResources resources", + current: BaseResources{Apps: 1}, + incoming: BaseResources{Apps: 2}, + expected: false, + }, + { + name: "equal BaseResources resources with unlimited values", + current: BaseResources{Apps: Unlimited}, + incoming: BaseResources{Apps: Unlimited}, + expected: true, + }, + { + name: "equal BaseResources with ComputeClasses and VolumeClasses", current: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, incoming: BaseResources{ - Apps: 2, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, - expected: false, + expected: true, }, { - name: "equal BaseResources resources with unlimited values", + name: "unequal BaseResources with ComputeClasses and VolumeClasses", current: BaseResources{ - Apps: Unlimited, - VolumeStorage: UnlimitedQuantity(), + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, incoming: BaseResources{ - Apps: Unlimited, - VolumeStorage: UnlimitedQuantity(), + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("2Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("2Mi")}}, }, - expected: true, + expected: false, }, } @@ -301,61 +338,64 @@ func TestBaseResourcesFits(t *testing.T) { incoming: BaseResources{}, }, { - name: "fits BaseResources", - current: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), - }, - incoming: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), - }, + name: "fits BaseResources", + current: BaseResources{Apps: 1}, + incoming: BaseResources{Apps: 1}, }, { - name: "does not fit BaseResources resources", - current: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), - }, - incoming: BaseResources{ - Apps: 2, - VolumeStorage: resource.MustParse("1Mi"), - }, + name: "does not fit BaseResources resources", + current: BaseResources{Apps: 1}, + incoming: BaseResources{Apps: 2}, expectedErr: ErrExceededResources, }, { - name: "fits BaseResources resources with specified unlimited values", - current: BaseResources{ - Apps: Unlimited, - VolumeStorage: UnlimitedQuantity(), - }, - incoming: BaseResources{ - Apps: 2, - VolumeStorage: resource.MustParse("2Mi"), - }, + name: "fits BaseResources resources with specified unlimited values", + current: BaseResources{Apps: Unlimited}, + incoming: BaseResources{Apps: 2}, + }, + { + name: "fits count BaseResources resources with specified unlimited values but not others", + current: BaseResources{Jobs: 0, Apps: Unlimited}, + incoming: BaseResources{Jobs: 2, Apps: 2}, + expectedErr: ErrExceededResources, }, { - name: "fits count BaseResources resources with specified unlimited values but not others", + name: "fits BaseResources with ComputeClasses and VolumeClasses", current: BaseResources{ - Jobs: 0, - Apps: Unlimited, + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, incoming: BaseResources{ - Jobs: 2, - Apps: 2, + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, - expectedErr: ErrExceededResources, }, - { - name: "fits quantity BaseResources resources with specified unlimited values but not others", + name: "does not fit exceeding ComputeClasses and VolumeClasses", current: BaseResources{ - VolumeStorage: UnlimitedQuantity(), + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, incoming: BaseResources{ - CPU: resource.MustParse("100m"), - VolumeStorage: resource.MustParse("2Mi"), + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("2Mi"), + CPU: resource.MustParse("2m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("2Mi")}}, }, expectedErr: ErrExceededResources, }, @@ -385,20 +425,47 @@ func TestBaseResourcesToString(t *testing.T) { expected: "", }, { - name: "populated BaseResources", + name: "populated BaseResources", + current: BaseResources{Apps: 1, Containers: 1}, + expected: "Apps: 1, Containers: 1", + }, + { + name: "populated BaseResources with unlimited values", + current: BaseResources{Apps: Unlimited, Containers: 1}, + expected: "Apps: unlimited, Containers: 1", + }, + { + name: "populated with ComputeClasses and VolumeClasses", current: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), - }, - expected: "Apps: 1, VolumeStorage: 1Mi", + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + }, + expected: "Apps: 1, Containers: 1, ComputeClasses: \"foo\": { Memory: 1Mi, CPU: 1m }, VolumeClasses: \"foo\": { VolumeStorage: 1Mi }", }, { - name: "populated BaseResources with unlimited values", + name: "populated with multiple ComputeClasses and VolumeClasses", current: BaseResources{ - Apps: Unlimited, - VolumeStorage: UnlimitedQuantity(), - }, - expected: "Apps: unlimited, VolumeStorage: unlimited", + Apps: 1, Containers: 1, + ComputeClasses: ComputeClassResources{ + "foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }, + "bar": { + Memory: resource.MustParse("2Mi"), + CPU: resource.MustParse("2m"), + }, + }, + VolumeClasses: VolumeClassResources{ + "foo": {resource.MustParse("1Mi")}, + "bar": {resource.MustParse("2Mi")}, + }, + }, + expected: "Apps: 1, Containers: 1, ComputeClasses: \"bar\": { Memory: 2Mi, CPU: 2m }, \"foo\": { Memory: 1Mi, CPU: 1m }, VolumeClasses: \"bar\": { VolumeStorage: 2Mi }, \"foo\": { VolumeStorage: 1Mi }", }, } diff --git a/pkg/apis/internal.admin.acorn.io/v1/computeclassresources.go b/pkg/apis/internal.admin.acorn.io/v1/computeclassresources.go new file mode 100644 index 000000000..9ea66620b --- /dev/null +++ b/pkg/apis/internal.admin.acorn.io/v1/computeclassresources.go @@ -0,0 +1,150 @@ +package v1 + +import ( + "fmt" + "strings" + + "github.com/acorn-io/baaah/pkg/typed" + "k8s.io/apimachinery/pkg/api/resource" +) + +// AllComputeClasses is a constant that can be used to define a ComputeResources struct that will apply to all +// ComputeClasses. This should only be used when defining a ComputeClassResources struct that is meant to be used +// as a limit and not a usage. The Fits method will work as expected when using this constant but Add and Remove +// do not interact with it. +const AllComputeClasses = "*" + +type ComputeResources struct { + Memory resource.Quantity `json:"memory,omitempty"` + CPU resource.Quantity `json:"cpu,omitempty"` +} + +func (current *ComputeResources) Equals(incoming ComputeResources) bool { + return current.Memory.Cmp(incoming.Memory) == 0 && current.CPU.Cmp(incoming.CPU) == 0 +} + +func (current *ComputeResources) ToString() string { + var resourceStrings []string + + for _, r := range []struct { + resource string + value resource.Quantity + }{ + {"Memory", current.Memory}, + {"CPU", current.CPU}, + } { + switch { + case r.value.CmpInt64(0) > 0: + resourceStrings = append(resourceStrings, fmt.Sprintf("%s: %s", r.resource, r.value.String())) + case r.value.Equal(comparableUnlimitedQuantity): + resourceStrings = append(resourceStrings, fmt.Sprintf("%s: unlimited", r.resource)) + } + } + + return strings.Join(resourceStrings, ", ") +} + +type ComputeClassResources map[string]ComputeResources + +// Add will add the ComputeClassResources of another ComputeClassResources struct into the current one. +func (current ComputeClassResources) Add(incoming ComputeClassResources) { + for computeClass, resources := range incoming { + c := current[computeClass] + c.Memory = AddQuantity(c.Memory, resources.Memory) + c.CPU = AddQuantity(c.CPU, resources.CPU) + current[computeClass] = c + } +} + +// Remove will remove the ComputeClassResources of another ComputeClassResources struct from the current one. Calling remove +// will be a no-op for any resource values that are set to unlimited. +func (current ComputeClassResources) Remove(incoming ComputeClassResources) { + for computeClass, resources := range incoming { + if _, ok := current[computeClass]; !ok { + continue + } + + c := current[computeClass] + c.Memory = SubQuantity(c.Memory, resources.Memory) + c.CPU = SubQuantity(c.CPU, resources.CPU) + + // Don't keep empty ComputeClasses + if c.Equals(ComputeResources{}) { + delete(current, computeClass) + } else { + current[computeClass] = c + } + } +} + +// Fits will check if a group of ComputeClassResources will be able to contain +// another group of ComputeClassResources. If the ComputeClassResources are not able to fit, +// an aggregated error will be returned with all exceeded ComputeClassResources. +// If the current ComputeClassResources defines unlimited, then it will always fit. +func (current ComputeClassResources) Fits(incoming ComputeClassResources) error { + var exceededResources []string + + // Check if any of the quantity resources are exceeded + for computeClass, resources := range incoming { + // If a specific compute class is defined on current then we check if it will + // fit the incoming resources. If is not defined, then we check if the current + // resources has AllComputeClasses defined and if so, we check if the incoming + // resources will fit those. If neither are defined, then we deny the request + // by appending the compute class to the exceeded resources and continuing. + if _, ok := current[computeClass]; !ok { + if _, ok := current[AllComputeClasses]; ok { + computeClass = AllComputeClasses + } + } + + var ccExceededResources []string + for _, r := range []struct { + resource string + current, incoming resource.Quantity + }{ + {"Memory", current[computeClass].Memory, resources.Memory}, + {"CPU", current[computeClass].CPU, resources.CPU}, + } { + if !FitsQuantity(r.current, r.incoming) { + ccExceededResources = append(ccExceededResources, r.resource) + } + } + if len(ccExceededResources) > 0 { + exceededResources = append(exceededResources, fmt.Sprintf("%q: %s", computeClass, strings.Join(ccExceededResources, ", "))) + } + } + + // Build an aggregated error message for the exceeded resources + if len(exceededResources) > 0 { + return fmt.Errorf("%w: ComputeClasses: %s", ErrExceededResources, strings.Join(exceededResources, ", ")) + } + + return nil +} + +// ToString will return a string representation of the ComputeClassResources within the struct. +func (current ComputeClassResources) ToString() string { + var resourceStrings []string + + for _, entry := range typed.Sorted(current) { + resourceStrings = append(resourceStrings, fmt.Sprintf("%q: { %s }", entry.Key, entry.Value.ToString())) + } + + return strings.Join(resourceStrings, ", ") +} + +// Equals will check if the current ComputeClassResources struct is equal to another. This is useful +// to avoid needing to do a deep equal on the entire struct. +func (current ComputeClassResources) Equals(incoming ComputeClassResources) bool { + if len(current) != len(incoming) { + return false + } + + for computeClass, resources := range incoming { + if cc, ok := current[computeClass]; !ok || !cc.Equals(resources) { + return false + } + } + + return true +} diff --git a/pkg/apis/internal.admin.acorn.io/v1/computeclassresources_test.go b/pkg/apis/internal.admin.acorn.io/v1/computeclassresources_test.go new file mode 100644 index 000000000..bf0d8c25c --- /dev/null +++ b/pkg/apis/internal.admin.acorn.io/v1/computeclassresources_test.go @@ -0,0 +1,467 @@ +package v1 + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/api/resource" +) + +func TestComputeClassResourcesAdd(t *testing.T) { + // Define test cases + testCases := []struct { + name string + current ComputeClassResources + incoming ComputeClassResources + expected ComputeClassResources + }{ + { + name: "add to empty ComputeClassResources resources", + current: ComputeClassResources{}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + expected: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + }, + { + name: "add to existing ComputeClassResources resources", + current: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + expected: ComputeClassResources{"foo": { + Memory: resource.MustParse("2Mi"), + }}, + }, + { + name: "add where current has a resource specified with unlimited", + current: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + expected: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + }, + { + name: "add where incoming has a resource specified with unlimited", + current: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + incoming: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + expected: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + }, + { + name: "add where current and incoming have a resource specified with unlimited", + current: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + incoming: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + expected: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + }, + { + name: "add where current and incoming have AllComputeClasses specified at non-unlimited values", + current: ComputeClassResources{AllComputeClasses: { + Memory: resource.MustParse("1Mi"), + }}, + incoming: ComputeClassResources{AllComputeClasses: { + Memory: resource.MustParse("1Mi"), + }}, + expected: ComputeClassResources{AllComputeClasses: { + Memory: resource.MustParse("2Mi"), + }}, + }, + } + + // Run the test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.current.Add(tc.incoming) + assert.True(t, tc.current.Equals(tc.expected)) + }) + } +} + +func TestComputeClassResourcesRemove(t *testing.T) { + // Define test cases + testCases := []struct { + name string + current ComputeClassResources + incoming ComputeClassResources + expected ComputeClassResources + }{ + { + name: "remove from empty ComputeClassResources resources", + current: ComputeClassResources{}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + expected: ComputeClassResources{}, + }, + { + name: "resulting empty does not remove other non-empty ComputeClassResources resources", + current: ComputeClassResources{ + "foo": {Memory: resource.MustParse("1Mi")}, + "bar": {Memory: resource.MustParse("2Mi")}, + }, + incoming: ComputeClassResources{ + "foo": {Memory: resource.MustParse("1Mi")}, + "bar": {Memory: resource.MustParse("1Mi")}, + }, + expected: ComputeClassResources{ + "bar": {Memory: resource.MustParse("1Mi")}, + }, + }, + { + name: "remove from existing ComputeClassResources resources", + current: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + expected: ComputeClassResources{}, + }, + { + name: "should never get negative values", + current: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("2Mi"), + }}, + expected: ComputeClassResources{}, + }, + { + name: "remove where current has a resource specified with unlimited", + current: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + expected: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + }, + { + name: "remove where incoming has a resource specified with unlimited", + current: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + incoming: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + expected: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + }, + { + name: "remove where current and incoming have a resource specified with unlimited", + current: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + incoming: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + expected: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + }, + { + name: "remove where current and incoming have a AllComputeClasses specified with non-unlimited values", + current: ComputeClassResources{AllComputeClasses: { + Memory: resource.MustParse("2Mi"), + }}, + incoming: ComputeClassResources{AllComputeClasses: { + Memory: resource.MustParse("1Mi"), + }}, + expected: ComputeClassResources{AllComputeClasses: { + Memory: resource.MustParse("1Mi"), + }}, + }, + { + name: "remove where current has two ComputeClasses and incoming has one", + current: ComputeClassResources{ + "foo": {Memory: resource.MustParse("2Mi")}, + "bar": {Memory: resource.MustParse("2Mi")}, + }, + incoming: ComputeClassResources{ + "foo": {Memory: resource.MustParse("2Mi")}, + }, + expected: ComputeClassResources{ + "bar": {Memory: resource.MustParse("2Mi")}, + }, + }, + } + + // Run the test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.current.Remove(tc.incoming) + assert.True(t, tc.current.Equals(tc.expected)) + }) + } +} + +func TestComputeClassResourcesEquals(t *testing.T) { + // Define test cases + testCases := []struct { + name string + current ComputeClassResources + incoming ComputeClassResources + expected bool + }{ + { + name: "empty ComputeClassResources resources", + current: ComputeClassResources{}, + incoming: ComputeClassResources{}, + expected: true, + }, + { + name: "equal ComputeClassResources resources", + current: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + expected: true, + }, + { + name: "unequal ComputeClassResources resources", + current: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("2Mi"), + }}, + expected: false, + }, + { + name: "equal ComputeClassResources resources with unlimited values", + current: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + incoming: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + expected: true, + }, + } + + // Run the test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.current.Equals(tc.incoming)) + }) + } +} + +func TestComputeClassResourcesFits(t *testing.T) { + // Define test cases + testCases := []struct { + name string + current ComputeClassResources + incoming ComputeClassResources + expectedErr error + }{ + { + name: "empty ComputeClassResources resources", + current: ComputeClassResources{}, + incoming: ComputeClassResources{}, + }, + { + name: "fits ComputeClassResources", + current: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + }, + { + name: "fits when incoming is empty", + current: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + }, + { + name: "does not fit when current is empty", + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + expectedErr: ErrExceededResources, + }, + { + name: "does not fit ComputeClassResources resources", + current: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + }}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("2Mi"), + }}, + expectedErr: ErrExceededResources, + }, + { + name: "fits ComputeClassResources resources with specified unlimited values", + current: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + }}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("2Mi"), + }}, + }, + { + name: "fits quantity ComputeClassResources resources with specified unlimited values but not others", + current: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + CPU: resource.MustParse("1m"), + }}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("2Mi"), + CPU: resource.MustParse("2m"), + }}, + expectedErr: ErrExceededResources, + }, + { + name: "fits ComputeClassResources with AllComputeClasses specified but not others", + current: ComputeClassResources{AllComputeClasses: { + Memory: resource.MustParse("2Mi"), + }}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("2Mi"), + }}, + }, + { + name: "fits ComputeClassResources with AllComputeClasses specified and others", + current: ComputeClassResources{AllComputeClasses: { + Memory: resource.MustParse("1Mi"), + }}, + incoming: ComputeClassResources{ + "foo": { + Memory: resource.MustParse("1Mi"), + }, + "bar": { + Memory: resource.MustParse("1Mi"), + }, + }, + }, + { + name: "fits ComputeClassResources with AllComputeClasses specified and others with unlimited set", + current: ComputeClassResources{AllComputeClasses: { + Memory: UnlimitedQuantity(), + }}, + incoming: ComputeClassResources{ + "foo": { + Memory: resource.MustParse("1Mi"), + }, + "bar": { + Memory: resource.MustParse("1Mi"), + }, + }, + }, + { + name: "does not fit ComputeClassResources with AllComputeClasses specified that is not enough", + current: ComputeClassResources{AllComputeClasses: { + Memory: resource.MustParse("1Mi"), + }}, + incoming: ComputeClassResources{"foo": { + Memory: resource.MustParse("2Mi"), + }}, + expectedErr: ErrExceededResources, + }, + { + name: "does not fit ComputeClassResources with AllComputeClasses if one incoming exceeds the resources", + current: ComputeClassResources{AllComputeClasses: { + Memory: resource.MustParse("1Mi"), + }}, + incoming: ComputeClassResources{ + "foo": { + Memory: resource.MustParse("2Mi"), + }, + "bar": { + Memory: resource.MustParse("1Mi"), + }, + }, + expectedErr: ErrExceededResources, + }, + } + + // Run the test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.current.Fits(tc.incoming) + if !errors.Is(err, tc.expectedErr) { + t.Errorf("expected %v, got %v", tc.expectedErr, err) + } + }) + } +} + +func TestComputeClassResourcesToString(t *testing.T) { + // Define test cases + testCases := []struct { + name string + current ComputeClassResources + expected string + }{ + { + name: "empty ComputeClassResources", + current: ComputeClassResources{}, + expected: "", + }, + { + name: "populated ComputeClassResources", + current: ComputeClassResources{"foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }}, + expected: "\"foo\": { Memory: 1Mi, CPU: 1m }", + }, + { + name: "populated ComputeClassResources with unlimited values", + current: ComputeClassResources{"foo": { + Memory: UnlimitedQuantity(), + CPU: UnlimitedQuantity(), + }}, + expected: "\"foo\": { Memory: unlimited, CPU: unlimited }", + }, + { + name: "multiple populated ComputeClassResources", + current: ComputeClassResources{ + "foo": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1m"), + }, + "bar": { + Memory: resource.MustParse("2Mi"), + CPU: resource.MustParse("2m"), + }, + }, + expected: "\"bar\": { Memory: 2Mi, CPU: 2m }, \"foo\": { Memory: 1Mi, CPU: 1m }", + }, + } + + // Run the test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.current.ToString()) + }) + } +} diff --git a/pkg/apis/internal.admin.acorn.io/v1/quotarequests.go b/pkg/apis/internal.admin.acorn.io/v1/quotarequests.go index 4f57d2fe1..ffa24da0e 100644 --- a/pkg/apis/internal.admin.acorn.io/v1/quotarequests.go +++ b/pkg/apis/internal.admin.acorn.io/v1/quotarequests.go @@ -110,9 +110,8 @@ func (current *QuotaRequestResources) Fits(incoming QuotaRequestResources) error // ToString will return a string representation of the QuotaRequestResources within the struct. func (current *QuotaRequestResources) ToString() string { - result := ResourcesToString( + result := CountResourcesToString( map[string]int{"Secrets": current.Secrets}, - nil, ) if result != "" { diff --git a/pkg/apis/internal.admin.acorn.io/v1/quotarequests_test.go b/pkg/apis/internal.admin.acorn.io/v1/quotarequests_test.go index 0dff933c3..1b0dfde9b 100644 --- a/pkg/apis/internal.admin.acorn.io/v1/quotarequests_test.go +++ b/pkg/apis/internal.admin.acorn.io/v1/quotarequests_test.go @@ -21,15 +21,17 @@ func TestQuotaRequestResourcesAdd(t *testing.T) { current: QuotaRequestResources{}, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, expected: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, @@ -38,26 +40,27 @@ func TestQuotaRequestResourcesAdd(t *testing.T) { name: "add to existing QuotaRequestResources", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {CPU: resource.MustParse("20m")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - Images: 1, - VolumeStorage: resource.MustParse("1Mi"), - CPU: resource.MustParse("20m"), + Apps: 1, + Images: 1, + ComputeClasses: ComputeClassResources{"compute-class": {CPU: resource.MustParse("20m")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, expected: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 2, - Images: 1, - VolumeStorage: resource.MustParse("2Mi"), - CPU: resource.MustParse("20m"), + Apps: 2, + Images: 1, + ComputeClasses: ComputeClassResources{"compute-class": {CPU: resource.MustParse("40m")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("2Mi")}}, }, Secrets: 2, }, @@ -66,22 +69,25 @@ func TestQuotaRequestResourcesAdd(t *testing.T) { name: "add where current has a resource specified with unlimited", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, expected: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, @@ -90,22 +96,25 @@ func TestQuotaRequestResourcesAdd(t *testing.T) { name: "add where incoming has a resource specified with unlimited", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, expected: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, @@ -114,22 +123,25 @@ func TestQuotaRequestResourcesAdd(t *testing.T) { name: "add where current and incoming have a resource specified with unlimited", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, expected: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, @@ -157,8 +169,9 @@ func TestQuotaRequestResourcesRemove(t *testing.T) { current: QuotaRequestResources{}, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, @@ -169,17 +182,17 @@ func TestQuotaRequestResourcesRemove(t *testing.T) { all: true, current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 2, - Memory: resource.MustParse("2Mi"), - VolumeStorage: resource.MustParse("2Mi"), + Apps: 2, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("2Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("2Mi")}}, }, Secrets: 2, }, @@ -191,13 +204,15 @@ func TestQuotaRequestResourcesRemove(t *testing.T) { name: "removes persistent resources with all", current: QuotaRequestResources{ BaseResources: BaseResources{ - VolumeStorage: resource.MustParse("1Mi"), + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - VolumeStorage: resource.MustParse("1Mi"), + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, @@ -210,19 +225,21 @@ func TestQuotaRequestResourcesRemove(t *testing.T) { name: "does not remove persistent resources without all", current: QuotaRequestResources{ BaseResources: BaseResources{ - VolumeStorage: resource.MustParse("1Mi"), + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - VolumeStorage: resource.MustParse("1Mi"), + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, expected: QuotaRequestResources{ BaseResources: BaseResources{ - VolumeStorage: resource.MustParse("1Mi"), + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, @@ -231,22 +248,25 @@ func TestQuotaRequestResourcesRemove(t *testing.T) { name: "remove where current has a resource specified with unlimited", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: 1, }, expected: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, @@ -255,22 +275,25 @@ func TestQuotaRequestResourcesRemove(t *testing.T) { name: "remove where incoming has a resource specified with unlimited", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, expected: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, @@ -279,20 +302,59 @@ func TestQuotaRequestResourcesRemove(t *testing.T) { name: "remove where current and incoming have a resource specified with unlimited", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, }, expected: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, + }, + }, + }, + { + name: "remove where current has two computeclasses and volumeclasses where incoming has one", + current: QuotaRequestResources{ + BaseResources: BaseResources{ + ComputeClasses: ComputeClassResources{ + "compute-class-1": {Memory: resource.MustParse("1Mi")}, + "compute-class-2": {Memory: resource.MustParse("1Mi")}, + }, + VolumeClasses: VolumeClassResources{ + "volume-class-1": {resource.MustParse("1Mi")}, + "volume-class-2": {resource.MustParse("1Mi")}, + }, + }, + }, + incoming: QuotaRequestResources{ + BaseResources: BaseResources{ + ComputeClasses: ComputeClassResources{ + "compute-class-1": {Memory: resource.MustParse("1Mi")}, + }, + VolumeClasses: VolumeClassResources{ + "volume-class-1": {resource.MustParse("1Mi")}, + }, + }, + }, + all: true, + expected: QuotaRequestResources{ + BaseResources: BaseResources{ + ComputeClasses: ComputeClassResources{ + "compute-class-2": {Memory: resource.MustParse("1Mi")}, + }, + VolumeClasses: VolumeClassResources{ + "volume-class-2": {resource.MustParse("1Mi")}, + }, }, }, }, @@ -323,15 +385,17 @@ func TestQuotaRequestResourcesEquals(t *testing.T) { name: "equal QuotaRequestResources", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, @@ -341,15 +405,17 @@ func TestQuotaRequestResourcesEquals(t *testing.T) { name: "unequal QuotaRequestResources only", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, }, expected: false, @@ -358,16 +424,18 @@ func TestQuotaRequestResourcesEquals(t *testing.T) { name: "unequal base resources only", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - Containers: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + Containers: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, @@ -377,15 +445,17 @@ func TestQuotaRequestResourcesEquals(t *testing.T) { name: "unequal QuotaRequestResources", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 2, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 2, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 2, }, @@ -395,15 +465,17 @@ func TestQuotaRequestResourcesEquals(t *testing.T) { name: "equal QuotaRequestResources with unlimited values", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - VolumeStorage: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - VolumeStorage: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, @@ -434,15 +506,17 @@ func TestQuotaRequestResourcesFits(t *testing.T) { name: "fits BaseResources", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, @@ -451,15 +525,17 @@ func TestQuotaRequestResourcesFits(t *testing.T) { name: "does not fit QuotaRequestResources", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 2, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 2, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 2, }, @@ -479,14 +555,16 @@ func TestQuotaRequestResourcesFits(t *testing.T) { name: "false as expected with only base resources", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 2, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 2, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("1Mi")}}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, }, expectedErr: ErrExceededResources, @@ -495,15 +573,16 @@ func TestQuotaRequestResourcesFits(t *testing.T) { name: "fits QuotaRequestResources with specified unlimited values", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - VolumeStorage: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 2, - VolumeStorage: resource.MustParse("2Mi"), + Apps: 2, + ComputeClasses: ComputeClassResources{"compute-class": {Memory: resource.MustParse("2Mi")}}, }, Secrets: 2, }, @@ -530,13 +609,13 @@ func TestQuotaRequestResourcesFits(t *testing.T) { name: "fits quantity QuotaRequestResources with specified unlimited values but not others", current: QuotaRequestResources{ BaseResources: BaseResources{ - VolumeStorage: UnlimitedQuantity(), + ComputeClasses: ComputeClassResources{"compute-class": {Memory: UnlimitedQuantity()}}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, }, incoming: QuotaRequestResources{ BaseResources: BaseResources{ - CPU: resource.MustParse("100m"), - VolumeStorage: resource.MustParse("2Mi"), + ComputeClasses: ComputeClassResources{"compute-class": {CPU: resource.MustParse("100m")}}, }, }, expectedErr: ErrExceededResources, @@ -569,23 +648,31 @@ func TestQuotaRequestResourcesToString(t *testing.T) { name: "populated BaseResources", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: 1, - VolumeStorage: resource.MustParse("1Mi"), + Apps: 1, + ComputeClasses: ComputeClassResources{"compute-class": { + Memory: resource.MustParse("1Mi"), + CPU: resource.MustParse("1Mi"), + }}, + VolumeClasses: VolumeClassResources{"volume-class": {resource.MustParse("1Mi")}}, }, Secrets: 1, }, - expected: "Secrets: 1, Apps: 1, VolumeStorage: 1Mi", + expected: "Secrets: 1, Apps: 1, ComputeClasses: \"compute-class\": { Memory: 1Mi, CPU: 1Mi }, VolumeClasses: \"volume-class\": { VolumeStorage: 1Mi }", }, { name: "populated BaseResources with unlimited values", current: QuotaRequestResources{ BaseResources: BaseResources{ - Apps: Unlimited, - VolumeStorage: UnlimitedQuantity(), + Apps: Unlimited, + ComputeClasses: ComputeClassResources{"compute-class": { + Memory: UnlimitedQuantity(), + CPU: UnlimitedQuantity(), + }}, + VolumeClasses: VolumeClassResources{"volume-class": {UnlimitedQuantity()}}, }, Secrets: Unlimited, }, - expected: "Secrets: unlimited, Apps: unlimited, VolumeStorage: unlimited", + expected: "Secrets: unlimited, Apps: unlimited, ComputeClasses: \"compute-class\": { Memory: unlimited, CPU: unlimited }, VolumeClasses: \"volume-class\": { VolumeStorage: unlimited }", }, } diff --git a/pkg/apis/internal.admin.acorn.io/v1/resources.go b/pkg/apis/internal.admin.acorn.io/v1/resources.go index 689885170..130711871 100644 --- a/pkg/apis/internal.admin.acorn.io/v1/resources.go +++ b/pkg/apis/internal.admin.acorn.io/v1/resources.go @@ -64,7 +64,7 @@ func SubQuantity(c, i resource.Quantity) resource.Quantity { } c.Sub(i) if c.CmpInt64(0) < 0 { - c.Set(0) + return *resource.NewQuantity(0, c.Format) } return c } @@ -83,9 +83,9 @@ func FitsQuantity(current, incoming resource.Quantity) bool { return true } -// ResourceToString will return a string representation of the resource and value +// CountResourcesToString will return a string representation of the resource and value // if its value is greater than 0. -func ResourcesToString(resources map[string]int, quantityResources map[string]resource.Quantity) string { +func CountResourcesToString(resources map[string]int) string { var resourceStrings []string for _, resource := range typed.Sorted(resources) { @@ -97,14 +97,15 @@ func ResourcesToString(resources map[string]int, quantityResources map[string]re } } - for _, resource := range typed.Sorted(quantityResources) { - switch { - case resource.Value.CmpInt64(0) > 0: - resourceStrings = append(resourceStrings, fmt.Sprintf("%s: %s", resource.Key, resource.Value.String())) - case resource.Value.Equal(comparableUnlimitedQuantity): - resourceStrings = append(resourceStrings, fmt.Sprintf("%s: unlimited", resource.Key)) - } - } - return strings.Join(resourceStrings, ", ") } + +func QuantityResourceToString(name string, quantity resource.Quantity) string { + switch { + case quantity.CmpInt64(0) > 0: + return fmt.Sprintf("%s: %s", name, quantity.String()) + case quantity.Equal(comparableUnlimitedQuantity): + return fmt.Sprintf("%s: unlimited", name) + } + return "" +} diff --git a/pkg/apis/internal.admin.acorn.io/v1/volumeclassresources.go b/pkg/apis/internal.admin.acorn.io/v1/volumeclassresources.go new file mode 100644 index 000000000..a6bee278a --- /dev/null +++ b/pkg/apis/internal.admin.acorn.io/v1/volumeclassresources.go @@ -0,0 +1,119 @@ +package v1 + +import ( + "fmt" + "strings" + + "github.com/acorn-io/baaah/pkg/typed" + "k8s.io/apimachinery/pkg/api/resource" +) + +// AllVolumeClasses is a constant that can be used to define a VolumeResources struct that will apply to all +// VolumeClasses. This should only be used when defining a VolumeClassResources struct that is meant to be used +// as a limit and not a usage. The Fits method will work as expected when using this constant but Add and Remove +// do not interact with it. +const AllVolumeClasses = "*" + +type VolumeResources struct { + VolumeStorage resource.Quantity `json:"volumeStorage"` +} + +func (current *VolumeResources) ToString() string { + switch { + case current.VolumeStorage.CmpInt64(0) > 0: + return "VolumeStorage: " + current.VolumeStorage.String() + case current.VolumeStorage.Equal(comparableUnlimitedQuantity): + return "VolumeStorage: unlimited" + } + return "" +} + +type VolumeClassResources map[string]VolumeResources + +// Add will add the VolumeClassResources of another VolumeClassResources struct into the current one. +func (current VolumeClassResources) Add(incoming VolumeClassResources) { + for volumeClass, resources := range incoming { + c := current[volumeClass] + c.VolumeStorage = AddQuantity(c.VolumeStorage, resources.VolumeStorage) + current[volumeClass] = c + } +} + +// Remove will remove the VolumeClassResources of another VolumeClassResources struct from the current one. Calling remove +// will be a no-op for any resource values that are set to unlimited. +func (current VolumeClassResources) Remove(incoming VolumeClassResources) { + for volumeClass, resources := range incoming { + if _, ok := current[volumeClass]; !ok { + continue + } + + c := current[volumeClass] + c.VolumeStorage = SubQuantity(c.VolumeStorage, resources.VolumeStorage) + + // Don't keep empty VolumeClasses + if c.VolumeStorage.CmpInt64(0) == 0 { + delete(current, volumeClass) + } else { + current[volumeClass] = c + } + } +} + +// Fits will check if a group of VolumeClassResources will be able to contain +// another group of VolumeClassResources. If the VolumeClassResources are not able to fit, +// an aggregated error will be returned with all exceeded VolumeClassResources. +// If the current VolumeClassResources defines unlimited, then it will always fit. +func (current VolumeClassResources) Fits(incoming VolumeClassResources) error { + var exceededResources []string + + // Check if any of the quantity resources are exceeded + for volumeClass, resources := range incoming { + // If a specific volume class is defined on current then we check if it will + // fit the incoming resources. If is not defined, then we check if the current + // resources has AllVolumeClasses defined and if so, we check if the incoming + // resources will fit those. If neither are defined, then we deny the request + // by appending the volume class to the exceeded resources and continuing. + if _, ok := current[volumeClass]; !ok { + if _, ok := current[AllVolumeClasses]; ok { + volumeClass = AllVolumeClasses + } + } + + if !FitsQuantity(current[volumeClass].VolumeStorage, resources.VolumeStorage) { + exceededResources = append(exceededResources, fmt.Sprintf("%q: VolumeStorage", volumeClass)) + } + } + + // Build an aggregated error message for the exceeded resources + if len(exceededResources) > 0 { + return fmt.Errorf("%w: VolumeClasses: %s", ErrExceededResources, strings.Join(exceededResources, ", ")) + } + + return nil +} + +// ToString will return a string representation of the VolumeClassResources within the struct. +func (current VolumeClassResources) ToString() string { + var resourceStrings []string + + for _, entry := range typed.Sorted(current) { + resourceStrings = append(resourceStrings, fmt.Sprintf("%q: { %s }", entry.Key, entry.Value.ToString())) + } + + return strings.Join(resourceStrings, ", ") +} + +// Equals will check if the current VolumeClassResources struct is equal to another. This is useful +// to avoid needing to do a deep equal on the entire struct. +func (current VolumeClassResources) Equals(incoming VolumeClassResources) bool { + if len(current) != len(incoming) { + return false + } + + for volumeClass, resources := range incoming { + if c, ok := current[volumeClass]; !ok || !c.VolumeStorage.Equal(resources.VolumeStorage) { + return false + } + } + return true +} diff --git a/pkg/apis/internal.admin.acorn.io/v1/volumeclassresources_test.go b/pkg/apis/internal.admin.acorn.io/v1/volumeclassresources_test.go new file mode 100644 index 000000000..a2e1cc7ad --- /dev/null +++ b/pkg/apis/internal.admin.acorn.io/v1/volumeclassresources_test.go @@ -0,0 +1,302 @@ +package v1 + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/api/resource" +) + +func TestVolumeClassResourcesAdd(t *testing.T) { + // Define test cases + testCases := []struct { + name string + current VolumeClassResources + incoming VolumeClassResources + expected VolumeClassResources + }{ + { + name: "add to empty VolumeClassResources resources", + current: VolumeClassResources{}, + incoming: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + expected: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + }, + { + name: "add to existing VolumeClassResources resources", + current: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + incoming: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + expected: VolumeClassResources{"foo": {resource.MustParse("2Mi")}}, + }, + { + name: "add where current has a resource specified with unlimited", + current: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + incoming: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + expected: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + }, + { + name: "add where incoming has a resource specified with unlimited", + current: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + incoming: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + expected: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + }, + { + name: "add where current and incoming have a resource specified with unlimited", + current: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + incoming: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + expected: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + }, + { + name: "add where current and incoming have a AllVolumeClasses specified with non-unlimited values", + current: VolumeClassResources{AllVolumeClasses: { + VolumeStorage: resource.MustParse("1Mi"), + }}, + incoming: VolumeClassResources{AllVolumeClasses: { + VolumeStorage: resource.MustParse("1Mi"), + }}, + expected: VolumeClassResources{AllVolumeClasses: { + VolumeStorage: resource.MustParse("2Mi"), + }}, + }, + } + + // Run the test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.current.Add(tc.incoming) + assert.True(t, tc.current.Equals(tc.expected)) + }) + } +} + +func TestVolumeClassResourcesRemove(t *testing.T) { + // Define test cases + testCases := []struct { + name string + current VolumeClassResources + incoming VolumeClassResources + expected VolumeClassResources + }{ + { + name: "remove from empty VolumeClassResources resources", + current: VolumeClassResources{}, + incoming: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + expected: VolumeClassResources{}, + }, + { + name: "remove from existing VolumeClassResources resources", + current: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + incoming: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + expected: VolumeClassResources{}, + }, + { + name: "should never get negative values", + current: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + incoming: VolumeClassResources{"foo": {resource.MustParse("2Mi")}}, + expected: VolumeClassResources{}, + }, + { + name: "remove where current has a resource specified with unlimited", + current: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + incoming: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + expected: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + }, + { + name: "remove where incoming has a resource specified with unlimited", + current: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + incoming: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + expected: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + }, + { + name: "remove where current and incoming have a resource specified with unlimited", + current: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + incoming: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + expected: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + }, + { + name: "remove where current and incoming have a AllVolumeClasses specified with non-unlimited values", + current: VolumeClassResources{AllVolumeClasses: { + VolumeStorage: resource.MustParse("2Mi"), + }}, + incoming: VolumeClassResources{AllVolumeClasses: { + VolumeStorage: resource.MustParse("1Mi"), + }}, + expected: VolumeClassResources{AllVolumeClasses: { + VolumeStorage: resource.MustParse("1Mi"), + }}, + }, + { + name: "remove where current has two volume classes and incoming has one", + current: VolumeClassResources{ + "foo": {resource.MustParse("2Mi")}, + "bar": {resource.MustParse("2Mi")}, + }, + incoming: VolumeClassResources{ + "foo": {resource.MustParse("2Mi")}, + }, + expected: VolumeClassResources{ + "bar": {resource.MustParse("2Mi")}, + }, + }, + } + + // Run the test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.current.Remove(tc.incoming) + assert.True(t, tc.current.Equals(tc.expected)) + }) + } +} + +func TestVolumeClassResourcesEquals(t *testing.T) { + // Define test cases + testCases := []struct { + name string + current VolumeClassResources + incoming VolumeClassResources + expected bool + }{ + { + name: "empty VolumeClassResources resources", + current: VolumeClassResources{}, + incoming: VolumeClassResources{}, + expected: true, + }, + { + name: "equal VolumeClassResources resources", + current: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + incoming: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + expected: true, + }, + { + name: "unequal VolumeClassResources resources", + current: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + incoming: VolumeClassResources{"foo": {resource.MustParse("2Mi")}}, + expected: false, + }, + { + name: "equal VolumeClassResources resources with unlimited values", + current: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + incoming: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + expected: true, + }, + } + + // Run the test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.current.Equals(tc.incoming)) + }) + } +} + +func TestVolumeClassResourcesFits(t *testing.T) { + // Define test cases + testCases := []struct { + name string + current VolumeClassResources + incoming VolumeClassResources + expectedErr error + }{ + { + name: "empty VolumeClassResources resources", + current: VolumeClassResources{}, + incoming: VolumeClassResources{}, + }, + { + name: "fits VolumeClassResources", + current: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + incoming: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + }, + + { + name: "does not fit VolumeClassResources resources", + current: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + incoming: VolumeClassResources{"foo": {resource.MustParse("2Mi")}}, + expectedErr: ErrExceededResources, + }, + { + name: "fits VolumeClassResources resources with specified unlimited values", + current: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + incoming: VolumeClassResources{"foo": {resource.MustParse("2Mi")}}, + }, + { + name: "fits VolumeClassResources with AllVolumeClasses specified but not others", + current: VolumeClassResources{AllVolumeClasses: {resource.MustParse("2Mi")}}, + incoming: VolumeClassResources{"foo": {resource.MustParse("2Mi")}}, + }, + { + name: "fits VolumeClassResources with AllVolumeClasses specified and others", + current: VolumeClassResources{AllVolumeClasses: {resource.MustParse("1Mi")}}, + incoming: VolumeClassResources{ + "foo": {resource.MustParse("1Mi")}, + "bar": {resource.MustParse("1Mi")}, + }, + }, + { + name: "fits VolumeClassResources with AllVolumeClasses specified and others with unlimited set", + current: VolumeClassResources{AllVolumeClasses: {UnlimitedQuantity()}}, + incoming: VolumeClassResources{ + "foo": {resource.MustParse("1Mi")}, + "bar": {resource.MustParse("1Mi")}, + }, + }, + { + name: "does not fit VolumeClassResources with AllVolumeClasses specified that is not enough", + current: VolumeClassResources{AllVolumeClasses: {resource.MustParse("1Mi")}}, + incoming: VolumeClassResources{"foo": {resource.MustParse("2Mi")}}, + expectedErr: ErrExceededResources, + }, + { + name: "does not fit VolumeClassResources with AllVolumeClasses if one incoming exceeds the resources", + current: VolumeClassResources{AllVolumeClasses: {resource.MustParse("1Mi")}}, + incoming: VolumeClassResources{ + "foo": {resource.MustParse("2Mi")}, + "bar": {resource.MustParse("1Mi")}, + }, + expectedErr: ErrExceededResources, + }, + } + + // Run the test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.current.Fits(tc.incoming) + if !errors.Is(err, tc.expectedErr) { + t.Errorf("expected %v, got %v", tc.expectedErr, err) + } + }) + } +} + +func TestVolumeClassResourcesToString(t *testing.T) { + // Define test cases + testCases := []struct { + name string + current VolumeClassResources + expected string + }{ + { + name: "empty VolumeClassResources", + current: VolumeClassResources{}, + expected: "", + }, + { + name: "populated VolumeClassResources", + current: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, + expected: "\"foo\": { VolumeStorage: 1Mi }", + }, + { + name: "populated VolumeClassResources with unlimited values", + current: VolumeClassResources{"foo": {UnlimitedQuantity()}}, + expected: "\"foo\": { VolumeStorage: unlimited }"}, + } + + // Run the test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.current.ToString()) + }) + } +} diff --git a/pkg/apis/internal.admin.acorn.io/v1/zz_generated.deepcopy.go b/pkg/apis/internal.admin.acorn.io/v1/zz_generated.deepcopy.go index edb131bc3..eea2c20fd 100644 --- a/pkg/apis/internal.admin.acorn.io/v1/zz_generated.deepcopy.go +++ b/pkg/apis/internal.admin.acorn.io/v1/zz_generated.deepcopy.go @@ -14,9 +14,20 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BaseResources) DeepCopyInto(out *BaseResources) { *out = *in - out.VolumeStorage = in.VolumeStorage.DeepCopy() - out.Memory = in.Memory.DeepCopy() - out.CPU = in.CPU.DeepCopy() + if in.ComputeClasses != nil { + in, out := &in.ComputeClasses, &out.ComputeClasses + *out = make(ComputeClassResources, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.VolumeClasses != nil { + in, out := &in.VolumeClasses, &out.VolumeClasses + *out = make(VolumeClassResources, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BaseResources. @@ -256,6 +267,44 @@ func (in *ComputeClassMemory) DeepCopy() *ComputeClassMemory { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ComputeClassResources) DeepCopyInto(out *ComputeClassResources) { + { + in := &in + *out = make(ComputeClassResources, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComputeClassResources. +func (in ComputeClassResources) DeepCopy() ComputeClassResources { + if in == nil { + return nil + } + out := new(ComputeClassResources) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComputeResources) DeepCopyInto(out *ComputeResources) { + *out = *in + out.Memory = in.Memory.DeepCopy() + out.CPU = in.CPU.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComputeResources. +func (in *ComputeResources) DeepCopy() *ComputeResources { + if in == nil { + return nil + } + out := new(ComputeResources) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ImageRoleAuthorizationInstance) DeepCopyInto(out *ImageRoleAuthorizationInstance) { *out = *in @@ -654,6 +703,27 @@ func (in *RoleRef) DeepCopy() *RoleRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in VolumeClassResources) DeepCopyInto(out *VolumeClassResources) { + { + in := &in + *out = make(VolumeClassResources, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeClassResources. +func (in VolumeClassResources) DeepCopy() VolumeClassResources { + if in == nil { + return nil + } + out := new(VolumeClassResources) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VolumeClassSize) DeepCopyInto(out *VolumeClassSize) { *out = *in @@ -668,3 +738,19 @@ func (in *VolumeClassSize) DeepCopy() *VolumeClassSize { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VolumeResources) DeepCopyInto(out *VolumeResources) { + *out = *in + out.VolumeStorage = in.VolumeStorage.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeResources. +func (in *VolumeResources) DeepCopy() *VolumeResources { + if in == nil { + return nil + } + out := new(VolumeResources) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controller/quota/quota.go b/pkg/controller/quota/quota.go index e8dd93edc..62d44612c 100644 --- a/pkg/controller/quota/quota.go +++ b/pkg/controller/quota/quota.go @@ -145,8 +145,15 @@ func addCompute(containers map[string]v1.Container, appInstance *v1.AppInstance, cpu.Mul(replicas(container.Scale)) memory.Mul(replicas(container.Scale)) - quotaRequest.Spec.Resources.CPU.Add(cpu) - quotaRequest.Spec.Resources.Memory.Add(memory) + // Add the compute resources to the quota request + computeClass := appInstance.Status.ResolvedOfferings.Containers[name].Class + quotaRequest.Spec.Resources.Add(adminv1.QuotaRequestResources{BaseResources: adminv1.BaseResources{ComputeClasses: adminv1.ComputeClassResources{ + computeClass: { + Memory: memory, + CPU: cpu, + }, + }, + }}) // Recurse over any sidecars. Since sidecars can't have sidecars, this is safe. addCompute(container.Sidecars, appInstance, quotaRequest) @@ -191,7 +198,11 @@ func addStorage(req router.Request, appInstance *v1.AppInstance, quotaRequest *a sizeQuantity = parsedQuantity } - quotaRequest.Spec.Resources.VolumeStorage.Add(sizeQuantity) + volumeClass := appInstance.Status.ResolvedOfferings.Volumes[name].Class + quotaRequest.Spec.Resources.Add(adminv1.QuotaRequestResources{ + BaseResources: adminv1.BaseResources{VolumeClasses: adminv1.VolumeClassResources{ + volumeClass: {VolumeStorage: sizeQuantity}, + }}}) } // Add the secrets needed to the quota request. We only parse net new secrets, not diff --git a/pkg/controller/quota/testdata/basic/expected.golden b/pkg/controller/quota/testdata/basic/expected.golden index 429072976..8f8bf90b4 100644 --- a/pkg/controller/quota/testdata/basic/expected.golden +++ b/pkg/controller/quota/testdata/basic/expected.golden @@ -9,23 +9,26 @@ metadata: spec: resources: apps: 0 + computeClasses: + default-compute-class: + cpu: 250m + memory: 1Gi containers: 1 - cpu: 250m images: 0 jobs: 1 - memory: 1Gi secrets: 1 - volumeStorage: 10G + volumeClasses: + default-volume-class: + volumeStorage: 10G volumes: 1 status: allocatedResources: apps: 0 + computeClasses: null containers: 0 - cpu: "0" images: 0 jobs: 0 - memory: "0" secrets: 0 - volumeStorage: "0" + volumeClasses: null volumes: 0 ` diff --git a/pkg/controller/quota/testdata/basic/input.yaml b/pkg/controller/quota/testdata/basic/input.yaml index 09be973b2..85d52c5e8 100644 --- a/pkg/controller/quota/testdata/basic/input.yaml +++ b/pkg/controller/quota/testdata/basic/input.yaml @@ -7,7 +7,7 @@ metadata: spec: image: image-name computeClass: - "": sample-compute-class + "": default-compute-class status: observedGeneration: 1 namespace: app-created-namespace @@ -54,17 +54,21 @@ status: resolvedOfferings: containers: "": - class: sample-compute-class + class: default-compute-class cpu: 125 memory: 536870912 container-name: - class: sample-compute-class + class: default-compute-class cpu: 125 memory: 536870912 sidecar-name: - class: sample-compute-class + class: default-compute-class cpu: 125 memory: 536870912 + volumes: + test: + class: default-volume-class + size: 536870912 scheduling: container-name: requirements: diff --git a/pkg/controller/quota/testdata/implicit-pv-bind/expected.golden b/pkg/controller/quota/testdata/implicit-pv-bind/expected.golden index 7963a424e..e008774b6 100644 --- a/pkg/controller/quota/testdata/implicit-pv-bind/expected.golden +++ b/pkg/controller/quota/testdata/implicit-pv-bind/expected.golden @@ -9,23 +9,24 @@ metadata: spec: resources: apps: 0 + computeClasses: + "": + cpu: "0" + memory: "0" containers: 1 - cpu: "0" images: 0 jobs: 0 - memory: "0" secrets: 1 - volumeStorage: "0" + volumeClasses: {} volumes: 1 status: allocatedResources: apps: 0 + computeClasses: null containers: 0 - cpu: "0" images: 0 jobs: 0 - memory: "0" secrets: 0 - volumeStorage: "0" + volumeClasses: null volumes: 0 ` diff --git a/pkg/controller/quota/testdata/status-default-volume-size/expected.golden b/pkg/controller/quota/testdata/status-default-volume-size/expected.golden index 71a04063a..57409264f 100644 --- a/pkg/controller/quota/testdata/status-default-volume-size/expected.golden +++ b/pkg/controller/quota/testdata/status-default-volume-size/expected.golden @@ -9,23 +9,26 @@ metadata: spec: resources: apps: 0 + computeClasses: + "": + cpu: "0" + memory: "0" containers: 1 - cpu: "0" images: 0 jobs: 0 - memory: "0" secrets: 1 - volumeStorage: 1Gi + volumeClasses: + "": + volumeStorage: 1Gi volumes: 1 status: allocatedResources: apps: 0 + computeClasses: null containers: 0 - cpu: "0" images: 0 jobs: 0 - memory: "0" secrets: 0 - volumeStorage: "0" + volumeClasses: null volumes: 0 ` diff --git a/pkg/openapi/generated/openapi_generated.go b/pkg/openapi/generated/openapi_generated.go index cbb47824f..a40cc2931 100644 --- a/pkg/openapi/generated/openapi_generated.go +++ b/pkg/openapi/generated/openapi_generated.go @@ -244,6 +244,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.ClusterVolumeClassInstance": schema_pkg_apis_internaladminacornio_v1_ClusterVolumeClassInstance(ref), "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.ClusterVolumeClassInstanceList": schema_pkg_apis_internaladminacornio_v1_ClusterVolumeClassInstanceList(ref), "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.ComputeClassMemory": schema_pkg_apis_internaladminacornio_v1_ComputeClassMemory(ref), + "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.ComputeResources": schema_pkg_apis_internaladminacornio_v1_ComputeResources(ref), "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.ImageRoleAuthorizationInstance": schema_pkg_apis_internaladminacornio_v1_ImageRoleAuthorizationInstance(ref), "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.ImageRoleAuthorizationInstanceList": schema_pkg_apis_internaladminacornio_v1_ImageRoleAuthorizationInstanceList(ref), "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.ImageRoleAuthorizationInstanceSpec": schema_pkg_apis_internaladminacornio_v1_ImageRoleAuthorizationInstanceSpec(ref), @@ -260,6 +261,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.RoleAuthorizations": schema_pkg_apis_internaladminacornio_v1_RoleAuthorizations(ref), "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.RoleRef": schema_pkg_apis_internaladminacornio_v1_RoleRef(ref), "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.VolumeClassSize": schema_pkg_apis_internaladminacornio_v1_VolumeClassSize(ref), + "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.VolumeResources": schema_pkg_apis_internaladminacornio_v1_VolumeResources(ref), "k8s.io/api/core/v1.AWSElasticBlockStoreVolumeSource": schema_k8sio_api_core_v1_AWSElasticBlockStoreVolumeSource(ref), "k8s.io/api/core/v1.Affinity": schema_k8sio_api_core_v1_Affinity(ref), "k8s.io/api/core/v1.AttachedVolume": schema_k8sio_api_core_v1_AttachedVolume(ref), @@ -14031,27 +14033,41 @@ func schema_pkg_apis_internaladminacornio_v1_BaseResources(ref common.ReferenceC Format: "int32", }, }, - "volumeStorage": { + "computeClasses": { SchemaProps: spec.SchemaProps{ - Ref: ref("k8s.io/apimachinery/pkg/api/resource.Quantity"), - }, - }, - "memory": { - SchemaProps: spec.SchemaProps{ - Ref: ref("k8s.io/apimachinery/pkg/api/resource.Quantity"), + Description: "ComputeClasses and VolumeClasses are used to track the amount of compute and volume storage per their respective classes", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.ComputeResources"), + }, + }, + }, }, }, - "cpu": { + "volumeClasses": { SchemaProps: spec.SchemaProps{ - Ref: ref("k8s.io/apimachinery/pkg/api/resource.Quantity"), + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.VolumeResources"), + }, + }, + }, }, }, }, - Required: []string{"apps", "containers", "jobs", "volumes", "images", "volumeStorage", "memory", "cpu"}, + Required: []string{"apps", "containers", "jobs", "volumes", "images", "computeClasses", "volumeClasses"}, }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/api/resource.Quantity"}, + "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.ComputeResources", "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.VolumeResources"}, } } @@ -14499,6 +14515,30 @@ func schema_pkg_apis_internaladminacornio_v1_ComputeClassMemory(ref common.Refer } } +func schema_pkg_apis_internaladminacornio_v1_ComputeResources(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "memory": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/api/resource.Quantity"), + }, + }, + "cpu": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/api/resource.Quantity"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/api/resource.Quantity"}, + } +} + func schema_pkg_apis_internaladminacornio_v1_ImageRoleAuthorizationInstance(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -15143,19 +15183,33 @@ func schema_pkg_apis_internaladminacornio_v1_QuotaRequestResources(ref common.Re Format: "int32", }, }, - "volumeStorage": { - SchemaProps: spec.SchemaProps{ - Ref: ref("k8s.io/apimachinery/pkg/api/resource.Quantity"), - }, - }, - "memory": { + "computeClasses": { SchemaProps: spec.SchemaProps{ - Ref: ref("k8s.io/apimachinery/pkg/api/resource.Quantity"), + Description: "ComputeClasses and VolumeClasses are used to track the amount of compute and volume storage per their respective classes", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.ComputeResources"), + }, + }, + }, }, }, - "cpu": { + "volumeClasses": { SchemaProps: spec.SchemaProps{ - Ref: ref("k8s.io/apimachinery/pkg/api/resource.Quantity"), + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.VolumeResources"), + }, + }, + }, }, }, "secrets": { @@ -15166,11 +15220,11 @@ func schema_pkg_apis_internaladminacornio_v1_QuotaRequestResources(ref common.Re }, }, }, - Required: []string{"apps", "containers", "jobs", "volumes", "images", "volumeStorage", "memory", "cpu", "secrets"}, + Required: []string{"apps", "containers", "jobs", "volumes", "images", "computeClasses", "volumeClasses", "secrets"}, }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/api/resource.Quantity"}, + "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.ComputeResources", "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1.VolumeResources"}, } } @@ -15269,6 +15323,26 @@ func schema_pkg_apis_internaladminacornio_v1_VolumeClassSize(ref common.Referenc } } +func schema_pkg_apis_internaladminacornio_v1_VolumeResources(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "volumeStorage": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/api/resource.Quantity"), + }, + }, + }, + Required: []string{"volumeStorage"}, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/api/resource.Quantity"}, + } +} + func schema_k8sio_api_core_v1_AWSElasticBlockStoreVolumeSource(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{