From fd0ed8bd04f66595b1bd6d114680ac5ec5537f3e Mon Sep 17 00:00:00 2001
From: Mykhailo Bobrovskyi
Date: Thu, 26 Sep 2024 14:48:33 +0300
Subject: [PATCH] Expose Flavors in LocalQueue Status.
---
apis/kueue/v1beta1/localqueue_types.go | 12 ++
apis/kueue/v1beta1/zz_generated.deepcopy.go | 20 +++
.../crd/kueue.x-k8s.io_localqueues.yaml | 18 +++
.../kueue/v1beta1/availableflavor.go | 42 ++++++
.../kueue/v1beta1/localqueuestatus.go | 14 ++
client-go/applyconfiguration/utils.go | 2 +
.../crd/bases/kueue.x-k8s.io_localqueues.yaml | 18 +++
pkg/cache/cache.go | 12 ++
pkg/controller/core/localqueue_controller.go | 1 +
.../en/docs/reference/kueue.v1beta1.md | 34 +++++
.../core/localqueue_controller_test.go | 121 +++++++++++++-----
11 files changed, 262 insertions(+), 32 deletions(-)
create mode 100644 client-go/applyconfiguration/kueue/v1beta1/availableflavor.go
diff --git a/apis/kueue/v1beta1/localqueue_types.go b/apis/kueue/v1beta1/localqueue_types.go
index 622014953a..9c59605a82 100644
--- a/apis/kueue/v1beta1/localqueue_types.go
+++ b/apis/kueue/v1beta1/localqueue_types.go
@@ -48,6 +48,11 @@ type LocalQueueSpec struct {
// +kubebuilder:validation:Pattern="^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"
type ClusterQueueReference string
+type AvailableFlavor struct {
+ // name of the flavor.
+ Name ResourceFlavorReference `json:"name"`
+}
+
// LocalQueueStatus defines the observed state of LocalQueue
type LocalQueueStatus struct {
// PendingWorkloads is the number of Workloads in the LocalQueue not yet admitted to a ClusterQueue
@@ -88,6 +93,13 @@ type LocalQueueStatus struct {
// +kubebuilder:validation:MaxItems=16
// +optional
FlavorUsage []LocalQueueFlavorUsage `json:"flavorUsage"`
+
+ // availableFlavors lists all currently available ResourceFlavors
+ // in specified ClusterQueue.
+ //
+ // +listType=map
+ // +listMapKey=name
+ AvailableFlavors []AvailableFlavor `json:"availableFlavors,omitempty"`
}
const (
diff --git a/apis/kueue/v1beta1/zz_generated.deepcopy.go b/apis/kueue/v1beta1/zz_generated.deepcopy.go
index cf773364d5..7aebcb7378 100644
--- a/apis/kueue/v1beta1/zz_generated.deepcopy.go
+++ b/apis/kueue/v1beta1/zz_generated.deepcopy.go
@@ -234,6 +234,21 @@ func (in *AdmissionChecksStrategy) DeepCopy() *AdmissionChecksStrategy {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AvailableFlavor) DeepCopyInto(out *AvailableFlavor) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AvailableFlavor.
+func (in *AvailableFlavor) DeepCopy() *AvailableFlavor {
+ if in == nil {
+ return nil
+ }
+ out := new(AvailableFlavor)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BorrowWithinCohort) DeepCopyInto(out *BorrowWithinCohort) {
*out = *in
@@ -707,6 +722,11 @@ func (in *LocalQueueStatus) DeepCopyInto(out *LocalQueueStatus) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.AvailableFlavors != nil {
+ in, out := &in.AvailableFlavors, &out.AvailableFlavors
+ *out = make([]AvailableFlavor, len(*in))
+ copy(*out, *in)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalQueueStatus.
diff --git a/charts/kueue/templates/crd/kueue.x-k8s.io_localqueues.yaml b/charts/kueue/templates/crd/kueue.x-k8s.io_localqueues.yaml
index d57d1941c6..c38410ad9e 100644
--- a/charts/kueue/templates/crd/kueue.x-k8s.io_localqueues.yaml
+++ b/charts/kueue/templates/crd/kueue.x-k8s.io_localqueues.yaml
@@ -106,6 +106,24 @@ spec:
admitted to a ClusterQueue and that haven't finished yet.
format: int32
type: integer
+ availableFlavors:
+ description: |-
+ availableFlavors lists all currently available ResourceFlavors
+ in specified ClusterQueue.
+ items:
+ properties:
+ name:
+ description: name of the flavor.
+ maxLength: 253
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ required:
+ - name
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
conditions:
description: |-
Conditions hold the latest available observations of the LocalQueue
diff --git a/client-go/applyconfiguration/kueue/v1beta1/availableflavor.go b/client-go/applyconfiguration/kueue/v1beta1/availableflavor.go
new file mode 100644
index 0000000000..1180191117
--- /dev/null
+++ b/client-go/applyconfiguration/kueue/v1beta1/availableflavor.go
@@ -0,0 +1,42 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+ v1beta1 "sigs.k8s.io/kueue/apis/kueue/v1beta1"
+)
+
+// AvailableFlavorApplyConfiguration represents a declarative configuration of the AvailableFlavor type for use
+// with apply.
+type AvailableFlavorApplyConfiguration struct {
+ Name *v1beta1.ResourceFlavorReference `json:"name,omitempty"`
+}
+
+// AvailableFlavorApplyConfiguration constructs a declarative configuration of the AvailableFlavor type for use with
+// apply.
+func AvailableFlavor() *AvailableFlavorApplyConfiguration {
+ return &AvailableFlavorApplyConfiguration{}
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *AvailableFlavorApplyConfiguration) WithName(value v1beta1.ResourceFlavorReference) *AvailableFlavorApplyConfiguration {
+ b.Name = &value
+ return b
+}
diff --git a/client-go/applyconfiguration/kueue/v1beta1/localqueuestatus.go b/client-go/applyconfiguration/kueue/v1beta1/localqueuestatus.go
index 38ca3bf58a..d2c1df8ab2 100644
--- a/client-go/applyconfiguration/kueue/v1beta1/localqueuestatus.go
+++ b/client-go/applyconfiguration/kueue/v1beta1/localqueuestatus.go
@@ -30,6 +30,7 @@ type LocalQueueStatusApplyConfiguration struct {
Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"`
FlavorsReservation []LocalQueueFlavorUsageApplyConfiguration `json:"flavorsReservation,omitempty"`
FlavorUsage []LocalQueueFlavorUsageApplyConfiguration `json:"flavorUsage,omitempty"`
+ AvailableFlavors []AvailableFlavorApplyConfiguration `json:"availableFlavors,omitempty"`
}
// LocalQueueStatusApplyConfiguration constructs a declarative configuration of the LocalQueueStatus type for use with
@@ -100,3 +101,16 @@ func (b *LocalQueueStatusApplyConfiguration) WithFlavorUsage(values ...*LocalQue
}
return b
}
+
+// WithAvailableFlavors adds the given value to the AvailableFlavors field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the AvailableFlavors field.
+func (b *LocalQueueStatusApplyConfiguration) WithAvailableFlavors(values ...*AvailableFlavorApplyConfiguration) *LocalQueueStatusApplyConfiguration {
+ for i := range values {
+ if values[i] == nil {
+ panic("nil value passed to WithAvailableFlavors")
+ }
+ b.AvailableFlavors = append(b.AvailableFlavors, *values[i])
+ }
+ return b
+}
diff --git a/client-go/applyconfiguration/utils.go b/client-go/applyconfiguration/utils.go
index 89e2bd5ca5..4d3c4f1490 100644
--- a/client-go/applyconfiguration/utils.go
+++ b/client-go/applyconfiguration/utils.go
@@ -67,6 +67,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &kueuev1beta1.AdmissionCheckStatusApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("AdmissionCheckStrategyRule"):
return &kueuev1beta1.AdmissionCheckStrategyRuleApplyConfiguration{}
+ case v1beta1.SchemeGroupVersion.WithKind("AvailableFlavor"):
+ return &kueuev1beta1.AvailableFlavorApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("BorrowWithinCohort"):
return &kueuev1beta1.BorrowWithinCohortApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("ClusterQueue"):
diff --git a/config/components/crd/bases/kueue.x-k8s.io_localqueues.yaml b/config/components/crd/bases/kueue.x-k8s.io_localqueues.yaml
index 768cf094a3..54f153d56d 100644
--- a/config/components/crd/bases/kueue.x-k8s.io_localqueues.yaml
+++ b/config/components/crd/bases/kueue.x-k8s.io_localqueues.yaml
@@ -91,6 +91,24 @@ spec:
admitted to a ClusterQueue and that haven't finished yet.
format: int32
type: integer
+ availableFlavors:
+ description: |-
+ availableFlavors lists all currently available ResourceFlavors
+ in specified ClusterQueue.
+ items:
+ properties:
+ name:
+ description: name of the flavor.
+ maxLength: 253
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ required:
+ - name
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
conditions:
description: |-
Conditions hold the latest available observations of the LocalQueue
diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go
index b28e81b1cd..f8b9fdbf77 100644
--- a/pkg/cache/cache.go
+++ b/pkg/cache/cache.go
@@ -38,6 +38,7 @@ import (
"sigs.k8s.io/kueue/pkg/hierarchy"
"sigs.k8s.io/kueue/pkg/metrics"
"sigs.k8s.io/kueue/pkg/resources"
+ "sigs.k8s.io/kueue/pkg/util/maps"
"sigs.k8s.io/kueue/pkg/workload"
)
@@ -668,6 +669,7 @@ type LocalQueueUsageStats struct {
ReservingWorkloads int
AdmittedResources []kueue.LocalQueueFlavorUsage
AdmittedWorkloads int
+ AvailableFlavors []kueue.AvailableFlavor
}
func (c *Cache) LocalQueueUsage(qObj *kueue.LocalQueue) (*LocalQueueUsageStats, error) {
@@ -683,11 +685,21 @@ func (c *Cache) LocalQueueUsage(qObj *kueue.LocalQueue) (*LocalQueueUsageStats,
return nil, errQNotFound
}
+ availableFlavors := make(map[kueue.ResourceFlavorReference]kueue.AvailableFlavor)
+ for _, rg := range cqImpl.ResourceGroups {
+ for _, fl := range rg.Flavors {
+ availableFlavors[fl] = kueue.AvailableFlavor{
+ Name: fl,
+ }
+ }
+ }
+
return &LocalQueueUsageStats{
ReservedResources: filterLocalQueueUsage(qImpl.usage, cqImpl.ResourceGroups),
ReservingWorkloads: qImpl.reservingWorkloads,
AdmittedResources: filterLocalQueueUsage(qImpl.admittedUsage, cqImpl.ResourceGroups),
AdmittedWorkloads: qImpl.admittedWorkloads,
+ AvailableFlavors: maps.Values(availableFlavors),
}, nil
}
diff --git a/pkg/controller/core/localqueue_controller.go b/pkg/controller/core/localqueue_controller.go
index fada128c34..a0820765c9 100644
--- a/pkg/controller/core/localqueue_controller.go
+++ b/pkg/controller/core/localqueue_controller.go
@@ -328,6 +328,7 @@ func (r *LocalQueueReconciler) UpdateStatusIfChanged(
queue.Status.AdmittedWorkloads = int32(stats.AdmittedWorkloads)
queue.Status.FlavorsReservation = stats.ReservedResources
queue.Status.FlavorUsage = stats.AdmittedResources
+ queue.Status.AvailableFlavors = stats.AvailableFlavors
if len(conditionStatus) != 0 && len(reason) != 0 && len(msg) != 0 {
meta.SetStatusCondition(&queue.Status.Conditions, metav1.Condition{
Type: kueue.LocalQueueActive,
diff --git a/site/content/en/docs/reference/kueue.v1beta1.md b/site/content/en/docs/reference/kueue.v1beta1.md
index 532cc77737..a08670d208 100644
--- a/site/content/en/docs/reference/kueue.v1beta1.md
+++ b/site/content/en/docs/reference/kueue.v1beta1.md
@@ -497,6 +497,30 @@ If empty, the AdmissionCheck will run for all workloads submitted to the Cluster
+## `AvailableFlavor` {#kueue-x-k8s-io-v1beta1-AvailableFlavor}
+
+
+**Appears in:**
+
+- [LocalQueueStatus](#kueue-x-k8s-io-v1beta1-LocalQueueStatus)
+
+
+
+
+
## `BorrowWithinCohort` {#kueue-x-k8s-io-v1beta1-BorrowWithinCohort}
@@ -1289,6 +1313,14 @@ workloads assigned to this LocalQueue.
workloads assigned to this LocalQueue.
+availableFlavors [Required]
+[]AvailableFlavor
+ |
+
+ availableFlavors lists all currently available ResourceFlavors
+in specified ClusterQueue.
+ |
+
@@ -1613,6 +1645,8 @@ this time would be reset to null.
- [AdmissionCheckStrategyRule](#kueue-x-k8s-io-v1beta1-AdmissionCheckStrategyRule)
+- [AvailableFlavor](#kueue-x-k8s-io-v1beta1-AvailableFlavor)
+
- [FlavorQuotas](#kueue-x-k8s-io-v1beta1-FlavorQuotas)
- [FlavorUsage](#kueue-x-k8s-io-v1beta1-FlavorUsage)
diff --git a/test/integration/controller/core/localqueue_controller_test.go b/test/integration/controller/core/localqueue_controller_test.go
index 8b6e95790f..1659729790 100644
--- a/test/integration/controller/core/localqueue_controller_test.go
+++ b/test/integration/controller/core/localqueue_controller_test.go
@@ -91,31 +91,62 @@ var _ = ginkgo.Describe("Queue controller", ginkgo.Ordered, ginkgo.ContinueOnFai
})
ginkgo.It("Should update conditions when clusterQueues that its localQueue references are updated", func() {
- gomega.Eventually(func() []metav1.Condition {
+ gomega.Eventually(func() kueue.LocalQueueStatus {
var updatedQueue kueue.LocalQueue
gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(queue), &updatedQueue)).To(gomega.Succeed())
- return updatedQueue.Status.Conditions
- }, util.Timeout, util.Interval).Should(gomega.BeComparableTo([]metav1.Condition{
- {
- Type: kueue.LocalQueueActive,
- Status: metav1.ConditionFalse,
- Reason: "ClusterQueueDoesNotExist",
- Message: "Can't submit new workloads to clusterQueue",
+ return updatedQueue.Status
+ }, util.Timeout, util.Interval).Should(gomega.BeComparableTo(kueue.LocalQueueStatus{
+ Conditions: []metav1.Condition{
+ {
+ Type: kueue.LocalQueueActive,
+ Status: metav1.ConditionFalse,
+ Reason: "ClusterQueueDoesNotExist",
+ Message: "Can't submit new workloads to clusterQueue",
+ },
},
}, util.IgnoreConditionTimestampsAndObservedGeneration))
+ emptyUsage := []kueue.LocalQueueFlavorUsage{
+ {
+ Name: flavorModelC,
+ Resources: []kueue.LocalQueueResourceUsage{
+ {
+ Name: resourceGPU,
+ Total: resource.MustParse("0"),
+ },
+ },
+ },
+ {
+ Name: flavorModelD,
+ Resources: []kueue.LocalQueueResourceUsage{
+ {
+ Name: resourceGPU,
+ Total: resource.MustParse("0"),
+ },
+ },
+ },
+ }
+
ginkgo.By("Creating a clusterQueue")
gomega.Expect(k8sClient.Create(ctx, clusterQueue)).To(gomega.Succeed())
- gomega.Eventually(func() []metav1.Condition {
+ gomega.Eventually(func() kueue.LocalQueueStatus {
var updatedQueue kueue.LocalQueue
gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(queue), &updatedQueue)).To(gomega.Succeed())
- return updatedQueue.Status.Conditions
- }, util.Timeout, util.Interval).Should(gomega.BeComparableTo([]metav1.Condition{
- {
- Type: kueue.LocalQueueActive,
- Status: metav1.ConditionFalse,
- Reason: "ClusterQueueIsInactive",
- Message: "Can't submit new workloads to clusterQueue",
+ return updatedQueue.Status
+ }, util.Timeout, util.Interval).Should(gomega.BeComparableTo(kueue.LocalQueueStatus{
+ Conditions: []metav1.Condition{
+ {
+ Type: kueue.LocalQueueActive,
+ Status: metav1.ConditionFalse,
+ Reason: "ClusterQueueIsInactive",
+ Message: "Can't submit new workloads to clusterQueue",
+ },
+ },
+ FlavorsReservation: emptyUsage,
+ FlavorUsage: emptyUsage,
+ AvailableFlavors: []kueue.AvailableFlavor{
+ {Name: flavorModelC},
+ {Name: flavorModelD},
},
}, util.IgnoreConditionTimestampsAndObservedGeneration))
@@ -135,31 +166,41 @@ var _ = ginkgo.Describe("Queue controller", ginkgo.Ordered, ginkgo.ContinueOnFai
Message: "Can admit new workloads",
},
}, util.IgnoreConditionTimestampsAndObservedGeneration))
- gomega.Eventually(func() []metav1.Condition {
+ gomega.Eventually(func() kueue.LocalQueueStatus {
var updatedQueue kueue.LocalQueue
gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(queue), &updatedQueue)).To(gomega.Succeed())
- return updatedQueue.Status.Conditions
- }, util.Timeout, util.Interval).Should(gomega.BeComparableTo([]metav1.Condition{
- {
- Type: kueue.LocalQueueActive,
- Status: metav1.ConditionTrue,
- Reason: "Ready",
- Message: "Can submit new workloads to clusterQueue",
+ return updatedQueue.Status
+ }, util.Timeout, util.Interval).Should(gomega.BeComparableTo(kueue.LocalQueueStatus{
+ Conditions: []metav1.Condition{
+ {
+ Type: kueue.LocalQueueActive,
+ Status: metav1.ConditionTrue,
+ Reason: "Ready",
+ Message: "Can submit new workloads to clusterQueue",
+ },
+ },
+ FlavorsReservation: emptyUsage,
+ FlavorUsage: emptyUsage,
+ AvailableFlavors: []kueue.AvailableFlavor{
+ {Name: flavorModelC},
+ {Name: flavorModelD},
},
}, util.IgnoreConditionTimestampsAndObservedGeneration))
ginkgo.By("Deleting a clusterQueue")
gomega.Expect(k8sClient.Delete(ctx, clusterQueue)).To(gomega.Succeed())
- gomega.Eventually(func() []metav1.Condition {
+ gomega.Eventually(func() kueue.LocalQueueStatus {
var updatedQueue kueue.LocalQueue
gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(queue), &updatedQueue)).To(gomega.Succeed())
- return updatedQueue.Status.Conditions
- }, util.Timeout, util.Interval).Should(gomega.BeComparableTo([]metav1.Condition{
- {
- Type: kueue.LocalQueueActive,
- Status: metav1.ConditionFalse,
- Reason: "ClusterQueueDoesNotExist",
- Message: "Can't submit new workloads to clusterQueue",
+ return updatedQueue.Status
+ }, util.Timeout, util.Interval).Should(gomega.BeComparableTo(kueue.LocalQueueStatus{
+ Conditions: []metav1.Condition{
+ {
+ Type: kueue.LocalQueueActive,
+ Status: metav1.ConditionFalse,
+ Reason: "ClusterQueueDoesNotExist",
+ Message: "Can't submit new workloads to clusterQueue",
+ },
},
}, util.IgnoreConditionTimestampsAndObservedGeneration))
})
@@ -239,6 +280,10 @@ var _ = ginkgo.Describe("Queue controller", ginkgo.Ordered, ginkgo.ContinueOnFai
},
FlavorsReservation: emptyUsage,
FlavorUsage: emptyUsage,
+ AvailableFlavors: []kueue.AvailableFlavor{
+ {Name: flavorModelC},
+ {Name: flavorModelD},
+ },
}, util.IgnoreConditionTimestampsAndObservedGeneration))
ginkgo.By("Setting the workloads quota reservation")
@@ -289,6 +334,10 @@ var _ = ginkgo.Describe("Queue controller", ginkgo.Ordered, ginkgo.ContinueOnFai
},
FlavorsReservation: fullUsage,
FlavorUsage: emptyUsage,
+ AvailableFlavors: []kueue.AvailableFlavor{
+ {Name: flavorModelC},
+ {Name: flavorModelD},
+ },
}, util.IgnoreConditionTimestampsAndObservedGeneration))
ginkgo.By("Setting the workloads admission checks")
@@ -314,6 +363,10 @@ var _ = ginkgo.Describe("Queue controller", ginkgo.Ordered, ginkgo.ContinueOnFai
},
FlavorsReservation: fullUsage,
FlavorUsage: fullUsage,
+ AvailableFlavors: []kueue.AvailableFlavor{
+ {Name: flavorModelC},
+ {Name: flavorModelD},
+ },
}, util.IgnoreConditionTimestampsAndObservedGeneration))
ginkgo.By("Finishing workloads")
@@ -333,6 +386,10 @@ var _ = ginkgo.Describe("Queue controller", ginkgo.Ordered, ginkgo.ContinueOnFai
},
FlavorsReservation: emptyUsage,
FlavorUsage: emptyUsage,
+ AvailableFlavors: []kueue.AvailableFlavor{
+ {Name: flavorModelC},
+ {Name: flavorModelD},
+ },
}, util.IgnoreConditionTimestampsAndObservedGeneration))
})
})