From 1644adbf05501bd6b27a53cfe2ae8a3d4c44abaa Mon Sep 17 00:00:00 2001 From: vladikkuzn Date: Wed, 8 May 2024 03:59:54 +0300 Subject: [PATCH] [kueuectl] List cluster queues --- cmd/kueuectl/app/cmd.go | 2 + cmd/kueuectl/app/list/list.go | 39 +++ cmd/kueuectl/app/list/list_clusterqueue.go | 268 ++++++++++++++++++ .../app/list/list_clusterqueue_test.go | 112 ++++++++ 4 files changed, 421 insertions(+) create mode 100644 cmd/kueuectl/app/list/list.go create mode 100644 cmd/kueuectl/app/list/list_clusterqueue.go create mode 100644 cmd/kueuectl/app/list/list_clusterqueue_test.go diff --git a/cmd/kueuectl/app/cmd.go b/cmd/kueuectl/app/cmd.go index d6b046b6fd..67f7fca665 100644 --- a/cmd/kueuectl/app/cmd.go +++ b/cmd/kueuectl/app/cmd.go @@ -24,6 +24,7 @@ import ( "k8s.io/cli-runtime/pkg/genericiooptions" "sigs.k8s.io/kueue/cmd/kueuectl/app/create" + "sigs.k8s.io/kueue/cmd/kueuectl/app/list" ) type KueuectlOptions struct { @@ -59,6 +60,7 @@ func NewKueuectlCmd(o KueuectlOptions) *cobra.Command { configFlags.AddFlags(flags) cmd.AddCommand(create.NewCreateCmd(configFlags, o.IOStreams)) + cmd.AddCommand(list.NewListCmd(configFlags, o.IOStreams)) return cmd } diff --git a/cmd/kueuectl/app/list/list.go b/cmd/kueuectl/app/list/list.go new file mode 100644 index 0000000000..31a359eefa --- /dev/null +++ b/cmd/kueuectl/app/list/list.go @@ -0,0 +1,39 @@ +/* +Copyright 2024 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package list + +import ( + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" +) + +const ( + listExample = ` # List LocalQueue + kueuectl list localqueue + # List ClusterQueue + kueuectl list cq|clusterqueue(s)` +) + +func NewListCmd(clientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Display resources", + Example: listExample, + } + + cmd.AddCommand(NewClusterQueueCmd(clientGetter, streams)) + + return cmd +} diff --git a/cmd/kueuectl/app/list/list_clusterqueue.go b/cmd/kueuectl/app/list/list_clusterqueue.go new file mode 100644 index 0000000000..2a182d4e06 --- /dev/null +++ b/cmd/kueuectl/app/list/list_clusterqueue.go @@ -0,0 +1,268 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package list + +import ( + "context" + "fmt" + "io" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" + "k8s.io/cli-runtime/pkg/printers" + cliflag "k8s.io/component-base/cli/flag" + + "sigs.k8s.io/kueue/apis/kueue/v1beta1" + "sigs.k8s.io/kueue/client-go/clientset/versioned" + "sigs.k8s.io/kueue/client-go/clientset/versioned/scheme" + kueuev1beta1 "sigs.k8s.io/kueue/client-go/clientset/versioned/typed/kueue/v1beta1" +) + +const ( + cqShort = `List ClusterQueues` + cqLong = `Lists all ClusterQueues, potentially limiting output to those that are active/inactive and matching the label selector.` + cqExample = ` # List ClusterQueue + kueuectl list cq|clusterqueue(s)` +) + +type clusterQueueActive string + +const ( + clusterQueueActiveAll clusterQueueActive = "*" + + // Truthy values + clusterQueueActiveTrue clusterQueueActive = "true" + + // Falsy values + clusterQueueActiveFalse clusterQueueActive = "false" +) + +// TODO: maybe create +// func (flags *ClusterQueueFlags) ToOptions(ctx context.Context, args []string) (*ClusterQueueOptions, error) { +// +// Run: func(cmd *cobra.Command, args []string) { +// o, err := flags.ToOptions(cmd.Context(), args) +// cmdutil.CheckErr(err) +// cmdutil.CheckErr(o.Run()) +// }, + +type ClusterQueueOptions struct { + // PrintFlags holds options necessary for obtaining a printer + PrintFlags *genericclioptions.PrintFlags + + LabelSelector string + FieldSelector string + + // Kueuectl flags + // Active is the flag to filter */true/false (all/active/inactive) cluster queues + // Active means the cluster queue has kueue.ClusterQueueActive condition with status=metav1.ConditionTrue + Active string + + Client kueuev1beta1.KueueV1beta1Interface + + PrintObj printers.ResourcePrinterFunc + + // ctx is the context for the command + ctx context.Context + + genericiooptions.IOStreams +} + +func NewClusterQueueOptions(streams genericiooptions.IOStreams) *ClusterQueueOptions { + return &ClusterQueueOptions{ + PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(scheme.Scheme), + IOStreams: streams, + } +} + +func NewClusterQueueCmd(clientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *cobra.Command { + o := NewClusterQueueOptions(streams) + + cmd := &cobra.Command{ + Use: "clusterqueue", + DisableFlagsInUseLine: true, + Aliases: []string{"cq"}, + Short: cqShort, + Long: cqLong, + Example: cqExample, + Run: func(cmd *cobra.Command, args []string) { + cobra.CheckErr(o.Complete(clientGetter, cmd, args)) + cobra.CheckErr(o.Validate()) + cobra.CheckErr(o.Run()) + }, + SuggestFor: []string{"ps"}, + } + + o.PrintFlags.AddFlags(cmd) + + fss := cliflag.NamedFlagSets{} + + filterFlagSet := fss.FlagSet("Filter") + filterFlagSet.StringVarP(&o.LabelSelector, "selector", "l", "", + "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.") + filterFlagSet.StringVar(&o.FieldSelector, "field-selector", "", + "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.") + + kueueFlagSet := fss.FlagSet("Kueue") + kueueFlagSet.StringVar(&o.Active, "active", "*", + "Filter by active status of cluster queues. Valid values are '*', 'true', 'false'.") + + fs := cmd.Flags() + for _, f := range fss.FlagSets { + fs.AddFlagSet(f) + } + + return cmd +} + +// Complete takes the command arguments and infers any remaining options. +func (o *ClusterQueueOptions) Complete(clientGetter genericclioptions.RESTClientGetter, cmd *cobra.Command, args []string) error { + var err error + o.ctx = cmd.Context() + + config, err := clientGetter.ToRESTConfig() + if err != nil { + return err + } + + clientset, err := versioned.NewForConfig(config) + if err != nil { + return err + } + + o.Client = clientset.KueueV1beta1() + + if !o.PrintFlags.OutputFlagSpecified() { + o.PrintObj = printTable + } else { + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return err + } + o.PrintObj = printer.PrintObj + } + + if len(args) > 0 { + activeFlag, err := cmd.Flags().GetString("active") + if err != nil { + return err + } + o.Active = activeFlag + } + + return nil +} + +func (o *ClusterQueueOptions) Validate() error { + if !o.validActiveFlagOptionProvided() { + return fmt.Errorf("invalid value for --active flag: %s", o.Active) + } + return nil +} + +func (o *ClusterQueueOptions) validActiveFlagOptionProvided() bool { + return o.Active == string(clusterQueueActiveAll) || + o.Active == string(clusterQueueActiveTrue) || + o.Active == string(clusterQueueActiveFalse) +} + +// Run prints the cluster queues. +func (o *ClusterQueueOptions) Run() error { + opts := metav1.ListOptions{LabelSelector: o.LabelSelector, FieldSelector: o.FieldSelector} + list, err := o.Client.ClusterQueues().List(o.ctx, opts) + if err != nil { + return err + } + + if len(list.Items) == 0 { + return nil + } + + o.applyActiveFilter(list) + + return o.PrintObj(list, o.Out) +} + +func (o *ClusterQueueOptions) applyActiveFilter(cql *v1beta1.ClusterQueueList) { + if o.Active == string(clusterQueueActiveAll) { + return + } + + filtered := make([]v1beta1.ClusterQueue, 0, len(cql.Items)) + for _, cq := range cql.Items { + switch o.Active { + case string(clusterQueueActiveTrue): + if isActiveStatus(&cq) { + filtered = append(filtered, cq) + } + case string(clusterQueueActiveFalse): + if !isActiveStatus(&cq) { + filtered = append(filtered, cq) + } + } + } + cql.Items = filtered +} + +// printTable is a printer function for ClusterQueueList objects. +var _ printers.ResourcePrinterFunc = printTable + +func printTable(obj runtime.Object, out io.Writer) error { + tp := printers.NewTablePrinter(printers.PrintOptions{}) + a := &metav1.Table{ + ColumnDefinitions: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Cohort", Type: "string"}, + {Name: "Pending Workloads", Type: "integer"}, + {Name: "Admitted Workloads", Type: "integer"}, + {Name: "Active", Type: "boolean"}, + {Name: "Age", Type: "string"}, + }, + Rows: toTableRows(obj.(*v1beta1.ClusterQueueList)), + } + return tp.PrintObj(a, out) +} + +func toTableRows(list *v1beta1.ClusterQueueList) []metav1.TableRow { + rows := make([]metav1.TableRow, len(list.Items)) + for index := range list.Items { + rows[index] = toTableRow(&list.Items[index]) + } + return rows +} + +func toTableRow(cq *v1beta1.ClusterQueue) metav1.TableRow { + return metav1.TableRow{ + Object: runtime.RawExtension{Object: cq}, + Cells: []interface{}{ + cq.Name, + cq.Spec.Cohort, + cq.Status.PendingWorkloads, + cq.Status.AdmittedWorkloads, + isActiveStatus(cq), + cq.CreationTimestamp, + }, + } +} + +func isActiveStatus(cq *v1beta1.ClusterQueue) bool { + return meta.IsStatusConditionPresentAndEqual(cq.Status.Conditions, v1beta1.ClusterQueueActive, metav1.ConditionTrue) +} diff --git a/cmd/kueuectl/app/list/list_clusterqueue_test.go b/cmd/kueuectl/app/list/list_clusterqueue_test.go new file mode 100644 index 0000000000..8ccdbc83cc --- /dev/null +++ b/cmd/kueuectl/app/list/list_clusterqueue_test.go @@ -0,0 +1,112 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package list + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/kueue/apis/kueue/v1beta1" + "sigs.k8s.io/kueue/client-go/clientset/versioned/fake" + utiltesting "sigs.k8s.io/kueue/pkg/util/testing" +) + +func TestClusterQueueRun(t *testing.T) { + testCases := map[string]struct { + o *ClusterQueueOptions + wantOut []string + wantErr error + }{ + "should print all cluster queue list": { + o: &ClusterQueueOptions{ + PrintObj: printTable, + Active: "*", + Client: fake.NewSimpleClientset( + utiltesting.MakeClusterQueue("cq1"). + Condition(v1beta1.ClusterQueueActive, metav1.ConditionTrue, "", ""). + Obj(), + utiltesting.MakeClusterQueue("cq2"). + Condition(v1beta1.ClusterQueueActive, metav1.ConditionFalse, "", ""). + Obj(), + utiltesting.MakeClusterQueue("cq3"). + Condition(v1beta1.ClusterQueueActive, metav1.ConditionUnknown, "", ""). + Obj(), + utiltesting.MakeClusterQueue("cq4"). + Condition("other", metav1.ConditionTrue, "", ""). + Obj(), + ).KueueV1beta1(), + }, + wantOut: []string{ + "NAME COHORT PENDING WORKLOADS ADMITTED WORKLOADS ACTIVE AGE", + "cq1 0 0 true 0001-01-01 00:00:00 +0000 UTC", + "cq2 0 0 false 0001-01-01 00:00:00 +0000 UTC", + "cq3 0 0 false 0001-01-01 00:00:00 +0000 UTC", + "cq4 0 0 false 0001-01-01 00:00:00 +0000 UTC\n", + }, + }, + "should print active cluster queue list": { + o: &ClusterQueueOptions{ + PrintObj: printTable, + Active: "true", + Client: fake.NewSimpleClientset( + utiltesting.MakeClusterQueue("cq1"). + Condition(v1beta1.ClusterQueueActive, metav1.ConditionTrue, "", ""). + Obj(), + utiltesting.MakeClusterQueue("cq2"). + Condition(v1beta1.ClusterQueueActive, metav1.ConditionFalse, "", ""). + Obj(), + utiltesting.MakeClusterQueue("cq3"). + Condition(v1beta1.ClusterQueueActive, metav1.ConditionUnknown, "", ""). + Obj(), + utiltesting.MakeClusterQueue("cq4"). + Condition("other", metav1.ConditionTrue, "", ""). + Obj(), + ).KueueV1beta1(), + }, + wantOut: []string{ + "NAME COHORT PENDING WORKLOADS ADMITTED WORKLOADS ACTIVE AGE", + "cq1 0 0 true 0001-01-01 00:00:00 +0000 UTC\n", + }, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + var bytesOut bytes.Buffer + + tc.o.Out = &bytesOut + tc.o.ErrOut = io.Discard + + gotErr := tc.o.Run() + if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.EquateErrors()); diff != "" { + t.Errorf("Unexpected error (-want/+got)\n%s", diff) + } + + gotOut := bytesOut.String() + joinedWantOut := strings.Join(tc.wantOut, "\n") + + if diff := cmp.Diff(joinedWantOut, gotOut); diff != "" { + t.Errorf("Unexpected output (-want/+got)\n%s", diff) + } + }) + } +}