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..cd657ad7f 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 incoming has a resource specified with 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 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,73 @@ 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", - current: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), - }, - incoming: BaseResources{ - Apps: 1, - Memory: resource.MustParse("1Mi"), - }, - expected: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), - }, + 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 incoming has a resource specified with unlimited", + name: "remove 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(), - }, - expected: 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")}}, }, + all: true, + expected: BaseResources{}, }, { - name: "remove where current and incoming have 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: Unlimited, - Memory: UnlimitedQuantity(), + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, incoming: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), - }, - expected: BaseResources{ - Apps: Unlimited, - Memory: UnlimitedQuantity(), + VolumeClasses: VolumeClassResources{"foo": {resource.MustParse("1Mi")}}, }, }, } @@ -242,40 +215,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 +296,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", + 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 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 +383,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..a84199cda --- /dev/null +++ b/pkg/apis/internal.admin.acorn.io/v1/computeclassresources_test.go @@ -0,0 +1,454 @@ +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"), + }}, + }, + } + + // 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..ebc46f2ea 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,23 @@ 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()}}, }, }, }, @@ -323,15 +349,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 +369,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 +388,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 +409,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 +429,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 +470,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 +489,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 +519,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 +537,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 +573,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 +612,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..0de5271d8 --- /dev/null +++ b/pkg/apis/internal.admin.acorn.io/v1/volumeclassresources_test.go @@ -0,0 +1,289 @@ +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"), + }}, + }, + } + + // 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 11834ed8a..379630e6d 100644 --- a/pkg/controller/quota/quota.go +++ b/pkg/controller/quota/quota.go @@ -124,7 +124,7 @@ func EnsureQuotaRequest(req router.Request, resp router.Response) error { // addContainers adds the number of containers and accounts for the scale of each container. func addContainers(containers map[string]v1.Container, quotaRequest *adminv1.QuotaRequestInstance) { for _, container := range containers { - quotaRequest.Spec.Resources.Containers += replicas(container.Scale) + quotaRequest.Spec.Resources.Containers += int(replicas(container.Scale)) } } @@ -139,11 +139,21 @@ func addCompute(containers map[string]v1.Container, appInstance *v1.AppInstance, requirements = all.Requirements } - // Add the memory/cpu requests to the quota request for each container at the scale specified - for i := 0; i < replicas(container.Scale); i++ { - quotaRequest.Spec.Resources.CPU.Add(requirements.Requests["cpu"]) - quotaRequest.Spec.Resources.Memory.Add(requirements.Requests["memory"]) - } + computeClass := appInstance.Status.ResolvedOfferings.Containers[name].Class + + // Multiply the memory/cpu requests by the scale of the container + cpu, memory := requirements.Requests["cpu"], requirements.Requests["memory"] + cpu.Mul(replicas(container.Scale)) + memory.Mul(replicas(container.Scale)) + + // Add the compute resources to the quota request + 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) @@ -188,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 @@ -256,9 +270,9 @@ func isEnforced(req router.Request, namespace string) (bool, error) { // replicas returns the number of replicas based on an int32 pointer. If the // pointer is nil, it is assumed to be 1. -func replicas(s *int32) int { +func replicas(s *int32) int64 { if s != nil { - return int(*s) + return int64(*s) } return 1 } 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 6e6680243..5de624358 100644 --- a/pkg/controller/quota/testdata/basic/input.yaml +++ b/pkg/controller/quota/testdata/basic/input.yaml @@ -66,3 +66,21 @@ status: requests: cpu: 125m memory: 512Mi + resolvedOfferings: + containers: + "": + class: default-compute-class + cpuScaler: 0.25 + memory: 123456789 + container-name: + class: default-compute-class + cpuScaler: 0.25 + memory: 536870912 + sidecar-name: + class: default-compute-class + cpuScaler: 0.25 + memory: 536870912 + volumes: + test: + class: default-volume-class + size: 536870912 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 8894dd84b..f931e36a9 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), @@ -14019,27 +14021,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"}, } } @@ -14487,6 +14503,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{ @@ -15131,19 +15171,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": { @@ -15154,11 +15208,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"}, } } @@ -15257,6 +15311,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{