From 5d9c711e8ebde509705765f1b870d28bedd10340 Mon Sep 17 00:00:00 2001 From: Mykhailo Bobrovskyi Date: Mon, 13 May 2024 17:14:48 +0300 Subject: [PATCH] [kueuectl] Add list LocalQueue command. (#2157) * [kueuectl] Add list LocalQueue command. * [kueuectl] Moved integration tests to unit. --- cmd/kueuectl/app/cmd.go | 11 +- cmd/kueuectl/app/create/create.go | 3 +- cmd/kueuectl/app/create/create_localqueue.go | 12 +- cmd/kueuectl/app/list/helpers.go | 41 +++ cmd/kueuectl/app/list/list.go | 41 +++ cmd/kueuectl/app/list/list_localqueue.go | 163 ++++++++++++ .../app/list/list_localqueue_printer.go | 89 +++++++ .../app/list/list_localqueue_printer_test.go | 85 +++++++ cmd/kueuectl/app/list/list_localqueue_test.go | 233 ++++++++++++++++++ cmd/kueuectl/app/list/list_test.go | 82 ++++++ .../update_workload_activation_options.go | 10 +- cmd/kueuectl/app/resume/resume.go | 3 +- cmd/kueuectl/app/resume/resume_workload.go | 4 +- cmd/kueuectl/app/stop/stop.go | 3 +- cmd/kueuectl/app/stop/stop_workload.go | 4 +- cmd/kueuectl/app/testing/fake.go | 39 +++ cmd/kueuectl/app/util/client_getter.go | 39 +++ pkg/util/testing/wrappers.go | 21 ++ .../en/docs/reference/kubectl-kueue/_index.md | 2 +- .../reference/kubectl-kueue/commands/list.md | 29 +++ test/integration/kueuectl/list_test.go | 82 ++++++ 21 files changed, 965 insertions(+), 31 deletions(-) create mode 100644 cmd/kueuectl/app/list/helpers.go create mode 100644 cmd/kueuectl/app/list/list.go create mode 100644 cmd/kueuectl/app/list/list_localqueue.go create mode 100644 cmd/kueuectl/app/list/list_localqueue_printer.go create mode 100644 cmd/kueuectl/app/list/list_localqueue_printer_test.go create mode 100644 cmd/kueuectl/app/list/list_localqueue_test.go create mode 100644 cmd/kueuectl/app/list/list_test.go create mode 100644 cmd/kueuectl/app/testing/fake.go create mode 100644 cmd/kueuectl/app/util/client_getter.go create mode 100644 site/content/en/docs/reference/kubectl-kueue/commands/list.md create mode 100644 test/integration/kueuectl/list_test.go diff --git a/cmd/kueuectl/app/cmd.go b/cmd/kueuectl/app/cmd.go index 5eef222e7b..32936da43b 100644 --- a/cmd/kueuectl/app/cmd.go +++ b/cmd/kueuectl/app/cmd.go @@ -24,8 +24,10 @@ import ( "k8s.io/cli-runtime/pkg/genericiooptions" "sigs.k8s.io/kueue/cmd/kueuectl/app/create" + "sigs.k8s.io/kueue/cmd/kueuectl/app/list" "sigs.k8s.io/kueue/cmd/kueuectl/app/resume" "sigs.k8s.io/kueue/cmd/kueuectl/app/stop" + "sigs.k8s.io/kueue/cmd/kueuectl/app/util" ) type KueuectlOptions struct { @@ -60,9 +62,12 @@ func NewKueuectlCmd(o KueuectlOptions) *cobra.Command { } configFlags.AddFlags(flags) - cmd.AddCommand(create.NewCreateCmd(configFlags, o.IOStreams)) - cmd.AddCommand(resume.NewResumeCmd(configFlags, o.IOStreams)) - cmd.AddCommand(stop.NewStopCmd(configFlags, o.IOStreams)) + clientGetter := util.NewClientGetter(configFlags) + + cmd.AddCommand(create.NewCreateCmd(clientGetter, o.IOStreams)) + cmd.AddCommand(resume.NewResumeCmd(clientGetter, o.IOStreams)) + cmd.AddCommand(stop.NewStopCmd(clientGetter, o.IOStreams)) + cmd.AddCommand(list.NewListCmd(clientGetter, o.IOStreams)) return cmd } diff --git a/cmd/kueuectl/app/create/create.go b/cmd/kueuectl/app/create/create.go index 8777fb0461..981d5bd76e 100644 --- a/cmd/kueuectl/app/create/create.go +++ b/cmd/kueuectl/app/create/create.go @@ -18,7 +18,6 @@ package create import ( "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "sigs.k8s.io/kueue/cmd/kueuectl/app/util" @@ -29,7 +28,7 @@ const ( kueuectl create localqueue my-local-queue -c my-cluster-queue` ) -func NewCreateCmd(clientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *cobra.Command { +func NewCreateCmd(clientGetter util.ClientGetter, streams genericiooptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Create a resource", diff --git a/cmd/kueuectl/app/create/create_localqueue.go b/cmd/kueuectl/app/create/create_localqueue.go index 2276268cb1..002bdc2419 100644 --- a/cmd/kueuectl/app/create/create_localqueue.go +++ b/cmd/kueuectl/app/create/create_localqueue.go @@ -27,7 +27,6 @@ import ( "k8s.io/cli-runtime/pkg/printers" "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" "sigs.k8s.io/kueue/cmd/kueuectl/app/util" @@ -68,7 +67,7 @@ func NewLocalQueueOptions(streams genericiooptions.IOStreams) *LocalQueueOptions } } -func NewLocalQueueCmd(clientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *cobra.Command { +func NewLocalQueueCmd(clientGetter util.ClientGetter, streams genericiooptions.IOStreams) *cobra.Command { o := NewLocalQueueOptions(streams) cmd := &cobra.Command{ @@ -101,7 +100,7 @@ func NewLocalQueueCmd(clientGetter genericclioptions.RESTClientGetter, streams g } // Complete completes all the required options -func (o *LocalQueueOptions) Complete(clientGetter genericclioptions.RESTClientGetter, cmd *cobra.Command, args []string) error { +func (o *LocalQueueOptions) Complete(clientGetter util.ClientGetter, cmd *cobra.Command, args []string) error { o.Name = args[0] var err error @@ -112,12 +111,7 @@ func (o *LocalQueueOptions) Complete(clientGetter genericclioptions.RESTClientGe o.ClusterQueue = v1beta1.ClusterQueueReference(o.UserSpecifiedClusterQueue) - config, err := clientGetter.ToRESTConfig() - if err != nil { - return err - } - - clientset, err := versioned.NewForConfig(config) + clientset, err := clientGetter.KueueClientSet() if err != nil { return err } diff --git a/cmd/kueuectl/app/list/helpers.go b/cmd/kueuectl/app/list/helpers.go new file mode 100644 index 0000000000..d840b543cb --- /dev/null +++ b/cmd/kueuectl/app/list/helpers.go @@ -0,0 +1,41 @@ +/* +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" +) + +func addFieldSelectorFlagVar(cmd *cobra.Command, p *string) { + cmd.Flags().StringVar(p, "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.") +} + +func addLabelSelectorFlagVar(cmd *cobra.Command, p *string) { + cmd.Flags().StringVarP(p, "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.") +} + +func addAllNamespacesFlagVar(cmd *cobra.Command, p *bool) { + cmd.Flags().BoolVarP(p, "all-namespaces", "A", false, + "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") +} + +func addClusterQueueFilterFlagVar(cmd *cobra.Command, p *string) { + cmd.Flags().StringVarP(p, "clusterqueue", "c", "", + "Filter by cluster queue name which associated with the local queue.") +} diff --git a/cmd/kueuectl/app/list/list.go b/cmd/kueuectl/app/list/list.go new file mode 100644 index 0000000000..15085fe351 --- /dev/null +++ b/cmd/kueuectl/app/list/list.go @@ -0,0 +1,41 @@ +/* +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/genericiooptions" + + "sigs.k8s.io/kueue/cmd/kueuectl/app/util" +) + +const ( + listExample = ` # List LocalQueue + kueuectl list localqueue` +) + +func NewListCmd(clientGetter util.ClientGetter, streams genericiooptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Display resources", + Example: listExample, + } + + cmd.AddCommand(NewLocalQueueCmd(clientGetter, streams)) + + return cmd +} diff --git a/cmd/kueuectl/app/list/list_localqueue.go b/cmd/kueuectl/app/list/list_localqueue.go new file mode 100644 index 0000000000..c253b54ea6 --- /dev/null +++ b/cmd/kueuectl/app/list/list_localqueue.go @@ -0,0 +1,163 @@ +/* +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" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" + "k8s.io/cli-runtime/pkg/printers" + + "sigs.k8s.io/kueue/apis/kueue/v1beta1" + "sigs.k8s.io/kueue/client-go/clientset/versioned/scheme" + kueuev1beta1 "sigs.k8s.io/kueue/client-go/clientset/versioned/typed/kueue/v1beta1" + "sigs.k8s.io/kueue/cmd/kueuectl/app/util" +) + +const ( + lqLong = `Lists LocalQueues that match the given criteria: point to a specific CQ, +being active/inactive, belonging to the specified namespace, matching +the label selector or the field selector.` + lqExample = ` # List LocalQueue + kueuectl list localqueue` +) + +type LocalQueueOptions struct { + PrintFlags *genericclioptions.PrintFlags + + AllNamespaces bool + Namespace string + FieldSelector string + LabelSelector string + ClusterQueueFilter string + + Client kueuev1beta1.KueueV1beta1Interface + + PrintObj printers.ResourcePrinterFunc + + genericiooptions.IOStreams +} + +func NewLocalQueueOptions(streams genericiooptions.IOStreams) *LocalQueueOptions { + return &LocalQueueOptions{ + PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(scheme.Scheme), + IOStreams: streams, + } +} + +func NewLocalQueueCmd(clientGetter util.ClientGetter, streams genericiooptions.IOStreams) *cobra.Command { + o := NewLocalQueueOptions(streams) + + cmd := &cobra.Command{ + Use: "localqueue [-–clusterqueue CLUSTER_QUEUE_NAME] [--selector key1=value1] [--field-selector key1=value1] [--all-namespaces]", + // To do not add "[flags]" suffix on the end of usage line + DisableFlagsInUseLine: true, + Aliases: []string{"lq"}, + Short: "List LocalQueue", + Long: lqLong, + Example: lqExample, + Run: func(cmd *cobra.Command, args []string) { + cobra.CheckErr(o.Complete(clientGetter, cmd, args)) + cobra.CheckErr(o.Run(cmd.Context())) + }, + } + + o.PrintFlags.AddFlags(cmd) + + addAllNamespacesFlagVar(cmd, &o.AllNamespaces) + addFieldSelectorFlagVar(cmd, &o.FieldSelector) + addLabelSelectorFlagVar(cmd, &o.LabelSelector) + addClusterQueueFilterFlagVar(cmd, &o.ClusterQueueFilter) + + return cmd +} + +// Complete completes all the required options +func (o *LocalQueueOptions) Complete(clientGetter util.ClientGetter, cmd *cobra.Command, args []string) error { + var err error + + o.Namespace, _, err = clientGetter.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + + clientset, err := clientGetter.KueueClientSet() + if err != nil { + return err + } + + o.Client = clientset.KueueV1beta1() + + if !o.PrintFlags.OutputFlagSpecified() { + printer := newLocalQueueTablePrinter() + if o.AllNamespaces { + printer.WithNamespace() + } + o.PrintObj = printer.PrintObj + } else { + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return err + } + o.PrintObj = printer.PrintObj + } + + return nil +} + +// Run performs the list operation. +func (o *LocalQueueOptions) Run(ctx context.Context) error { + namespace := o.Namespace + if o.AllNamespaces { + namespace = "" + } + + opts := metav1.ListOptions{LabelSelector: o.LabelSelector, FieldSelector: o.FieldSelector} + list, err := o.Client.LocalQueues(namespace).List(ctx, opts) + if err != nil { + return err + } + + o.filterList(list) + + if len(list.Items) == 0 { + if !o.AllNamespaces { + fmt.Fprintf(o.ErrOut, "No resources found in %s namespace.\n", o.Namespace) + } else { + fmt.Fprintln(o.ErrOut, "No resources found") + } + return nil + } + + return o.PrintObj(list, o.Out) +} + +func (o *LocalQueueOptions) filterList(list *v1beta1.LocalQueueList) { + if len(o.ClusterQueueFilter) > 0 { + filteredItems := make([]v1beta1.LocalQueue, 0, len(o.ClusterQueueFilter)) + for _, lq := range list.Items { + if lq.Spec.ClusterQueue == v1beta1.ClusterQueueReference(o.ClusterQueueFilter) { + filteredItems = append(filteredItems, lq) + } + } + list.Items = filteredItems + } +} diff --git a/cmd/kueuectl/app/list/list_localqueue_printer.go b/cmd/kueuectl/app/list/list_localqueue_printer.go new file mode 100644 index 0000000000..0aa36d06d4 --- /dev/null +++ b/cmd/kueuectl/app/list/list_localqueue_printer.go @@ -0,0 +1,89 @@ +/* +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 ( + "fmt" + "io" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/cli-runtime/pkg/printers" + + "sigs.k8s.io/kueue/apis/kueue/v1beta1" +) + +type listLocalQueuePrinter struct { + printOptions printers.PrintOptions +} + +var _ printers.ResourcePrinter = (*listLocalQueuePrinter)(nil) + +func (p *listLocalQueuePrinter) PrintObj(obj runtime.Object, out io.Writer) error { + printer := printers.NewTablePrinter(p.printOptions) + + list, ok := obj.(*v1beta1.LocalQueueList) + if !ok { + return fmt.Errorf("invalid object type") + } + + table := &metav1.Table{ + ColumnDefinitions: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "ClusterQueue", Type: "string"}, + {Name: "Pending Workloads", Type: "integer"}, + {Name: "Admitted Workloads", Type: "integer"}, + {Name: "Age", Type: "string"}, + }, + Rows: printLocalQueueList(list), + } + + return printer.PrintObj(table, out) +} + +func (p *listLocalQueuePrinter) WithNamespace() *listLocalQueuePrinter { + p.printOptions.WithNamespace = true + return p +} + +func newLocalQueueTablePrinter() *listLocalQueuePrinter { + return &listLocalQueuePrinter{} +} + +func printLocalQueueList(list *v1beta1.LocalQueueList) []metav1.TableRow { + rows := make([]metav1.TableRow, len(list.Items)) + for index := range list.Items { + rows[index] = printLocalQueue(&list.Items[index]) + } + return rows +} + +func printLocalQueue(localQueue *v1beta1.LocalQueue) metav1.TableRow { + row := metav1.TableRow{ + Object: runtime.RawExtension{Object: localQueue}, + } + row.Cells = []any{ + localQueue.Name, + localQueue.Spec.ClusterQueue, + localQueue.Status.PendingWorkloads, + localQueue.Status.AdmittedWorkloads, + duration.HumanDuration(time.Since(localQueue.CreationTimestamp.Time)), + } + return row +} diff --git a/cmd/kueuectl/app/list/list_localqueue_printer_test.go b/cmd/kueuectl/app/list/list_localqueue_printer_test.go new file mode 100644 index 0000000000..b02e17b64e --- /dev/null +++ b/cmd/kueuectl/app/list/list_localqueue_printer_test.go @@ -0,0 +1,85 @@ +/* +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 ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "sigs.k8s.io/kueue/apis/kueue/v1beta1" +) + +func TestLocalQueuePrint(t *testing.T) { + testStartTime := time.Now() + + testCases := map[string]struct { + options *LocalQueueOptions + in *v1beta1.LocalQueueList + out []metav1.TableRow + }{ + "should print local queue list": { + options: &LocalQueueOptions{}, + in: &v1beta1.LocalQueueList{ + Items: []v1beta1.LocalQueue{ + { + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "lq", + CreationTimestamp: metav1.NewTime(testStartTime.Add(-time.Hour).Truncate(time.Second)), + }, + Spec: v1beta1.LocalQueueSpec{ClusterQueue: "cq1"}, + Status: v1beta1.LocalQueueStatus{ + PendingWorkloads: 1, + AdmittedWorkloads: 2, + }, + }, + }, + }, + out: []metav1.TableRow{ + { + Cells: []any{"lq", v1beta1.ClusterQueueReference("cq1"), int32(1), int32(2), "60m"}, + Object: runtime.RawExtension{ + Object: &v1beta1.LocalQueue{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "lq", + CreationTimestamp: metav1.NewTime(testStartTime.Add(-time.Hour).Truncate(time.Second)), + }, + Spec: v1beta1.LocalQueueSpec{ClusterQueue: "cq1"}, + Status: v1beta1.LocalQueueStatus{ + PendingWorkloads: 1, + AdmittedWorkloads: 2, + }, + }, + }, + }, + }, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + out := printLocalQueueList(tc.in) + if diff := cmp.Diff(tc.out, out); diff != "" { + t.Errorf("Unexpected result (-want,+got):\n%s", diff) + } + }) + } +} diff --git a/cmd/kueuectl/app/list/list_localqueue_test.go b/cmd/kueuectl/app/list/list_localqueue_test.go new file mode 100644 index 0000000000..be556a1dae --- /dev/null +++ b/cmd/kueuectl/app/list/list_localqueue_test.go @@ -0,0 +1,233 @@ +/* +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 ( + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericiooptions" + + "sigs.k8s.io/kueue/apis/kueue/v1beta1" + "sigs.k8s.io/kueue/client-go/clientset/versioned/fake" + cmdtesting "sigs.k8s.io/kueue/cmd/kueuectl/app/testing" + utiltesting "sigs.k8s.io/kueue/pkg/util/testing" +) + +const defaultNamespace = "default" + +func TestLocalQueueFilter(t *testing.T) { + testCases := map[string]struct { + options *LocalQueueOptions + in *v1beta1.LocalQueueList + out *v1beta1.LocalQueueList + }{ + "shouldn't filter": { + options: &LocalQueueOptions{}, + in: &v1beta1.LocalQueueList{ + Items: []v1beta1.LocalQueue{ + { + Spec: v1beta1.LocalQueueSpec{ClusterQueue: "cq1"}, + }, + { + Spec: v1beta1.LocalQueueSpec{ClusterQueue: "cq2"}, + }, + }, + }, + out: &v1beta1.LocalQueueList{ + Items: []v1beta1.LocalQueue{ + { + Spec: v1beta1.LocalQueueSpec{ClusterQueue: "cq1"}, + }, + { + Spec: v1beta1.LocalQueueSpec{ClusterQueue: "cq2"}, + }, + }, + }, + }, + "should filter by cluster queue": { + options: &LocalQueueOptions{ + ClusterQueueFilter: "cq1", + }, + in: &v1beta1.LocalQueueList{ + Items: []v1beta1.LocalQueue{ + { + Spec: v1beta1.LocalQueueSpec{ClusterQueue: "cq1"}, + }, + { + Spec: v1beta1.LocalQueueSpec{ClusterQueue: "cq2"}, + }, + }, + }, + out: &v1beta1.LocalQueueList{ + Items: []v1beta1.LocalQueue{ + { + Spec: v1beta1.LocalQueueSpec{ClusterQueue: "cq1"}, + }, + }, + }, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.options.filterList(tc.in) + if diff := cmp.Diff(tc.out, tc.in); diff != "" { + t.Errorf("Unexpected result (-want,+got):\n%s", diff) + } + }) + } +} + +func TestLocalQueueCmd(t *testing.T) { + testStartTime := time.Now() + + testCases := map[string]struct { + ns string + objs []runtime.Object + args []string + wantOut string + wantOutErr string + wantErr error + }{ + "should print local queue list with namespace filter": { + ns: "ns1", + objs: []runtime.Object{ + utiltesting.MakeLocalQueue("lq1", "ns1"). + ClusterQueue("cq1"). + PendingWorkloads(1). + AdmittedWorkloads(1). + Creation(testStartTime.Add(-1 * time.Hour).Truncate(time.Second)). + Obj(), + utiltesting.MakeLocalQueue("lq2", "ns2"). + ClusterQueue("cq2"). + PendingWorkloads(2). + AdmittedWorkloads(2). + Creation(testStartTime.Add(-2 * time.Hour).Truncate(time.Second)). + Obj(), + }, + wantOut: `NAME CLUSTERQUEUE PENDING WORKLOADS ADMITTED WORKLOADS AGE +lq1 cq1 1 1 60m +`, + }, + "should print local queue list with clusterqueue filter": { + args: []string{"--clusterqueue", "cq1"}, + objs: []runtime.Object{ + utiltesting.MakeLocalQueue("lq1", defaultNamespace). + ClusterQueue("cq1"). + PendingWorkloads(1). + AdmittedWorkloads(1). + Creation(testStartTime.Add(-1 * time.Hour).Truncate(time.Second)). + Obj(), + utiltesting.MakeLocalQueue("lq2", defaultNamespace). + ClusterQueue("cq2"). + PendingWorkloads(2). + AdmittedWorkloads(2). + Creation(testStartTime.Add(-2 * time.Hour).Truncate(time.Second)). + Obj(), + }, + wantOut: `NAME CLUSTERQUEUE PENDING WORKLOADS ADMITTED WORKLOADS AGE +lq1 cq1 1 1 60m +`, + }, + "should print local queue list with label selector filter": { + args: []string{"--selector", "key=value1"}, + objs: []runtime.Object{ + utiltesting.MakeLocalQueue("lq1", defaultNamespace). + ClusterQueue("cq1"). + PendingWorkloads(1). + AdmittedWorkloads(1). + Label("key", "value1"). + Creation(testStartTime.Add(-1 * time.Hour).Truncate(time.Second)). + Obj(), + utiltesting.MakeLocalQueue("lq2", defaultNamespace). + ClusterQueue("cq2"). + PendingWorkloads(2). + AdmittedWorkloads(2). + Creation(testStartTime.Add(-2*time.Hour).Truncate(time.Second)). + Label("key", "value2"). + Obj(), + }, + wantOut: `NAME CLUSTERQUEUE PENDING WORKLOADS ADMITTED WORKLOADS AGE +lq1 cq1 1 1 60m +`, + }, + "should print local queue list with label selector filter (short flag)": { + args: []string{"-l", "foo=bar"}, + objs: []runtime.Object{ + utiltesting.MakeLocalQueue("lq1", defaultNamespace). + ClusterQueue("cq1"). + PendingWorkloads(1). + AdmittedWorkloads(1). + Label("foo", "bar"). + Creation(testStartTime.Add(-1 * time.Hour).Truncate(time.Second)). + Obj(), + utiltesting.MakeLocalQueue("lq2", defaultNamespace). + ClusterQueue("cq2"). + PendingWorkloads(2). + AdmittedWorkloads(2). + Creation(testStartTime.Add(-2 * time.Hour).Truncate(time.Second)). + Obj(), + }, + wantOut: `NAME CLUSTERQUEUE PENDING WORKLOADS ADMITTED WORKLOADS AGE +lq1 cq1 1 1 60m +`, + }, + "should print not found error": { + wantOutErr: fmt.Sprintf("No resources found in %s namespace.\n", defaultNamespace), + }, + "should print not found error with all-namespaces filter": { + args: []string{"-A"}, + wantOutErr: "No resources found\n", + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + streams, _, out, outErr := genericiooptions.NewTestIOStreams() + + tf := cmdtesting.NewTestClientGetter() + if len(tc.ns) > 0 { + tf.WithNamespace(tc.ns) + } else { + tf.WithNamespace(defaultNamespace) + } + + tf.ClientSet = fake.NewSimpleClientset(tc.objs...) + + cmd := NewLocalQueueCmd(tf, streams) + cmd.SetArgs(tc.args) + + gotErr := cmd.Execute() + if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.EquateErrors()); diff != "" { + t.Errorf("Unexpected error (-want/+got)\n%s", diff) + } + + gotOut := out.String() + if diff := cmp.Diff(tc.wantOut, gotOut); diff != "" { + t.Errorf("Unexpected output (-want/+got)\n%s", diff) + } + + gotOutErr := outErr.String() + if diff := cmp.Diff(tc.wantOutErr, gotOutErr); diff != "" { + t.Errorf("Unexpected output (-want/+got)\n%s", diff) + } + }) + } +} diff --git a/cmd/kueuectl/app/list/list_test.go b/cmd/kueuectl/app/list/list_test.go new file mode 100644 index 0000000000..bbda58452d --- /dev/null +++ b/cmd/kueuectl/app/list/list_test.go @@ -0,0 +1,82 @@ +package list + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericiooptions" + + "sigs.k8s.io/kueue/client-go/clientset/versioned/fake" + cmdtesting "sigs.k8s.io/kueue/cmd/kueuectl/app/testing" + utiltesting "sigs.k8s.io/kueue/pkg/util/testing" +) + +func TestListCmd(t *testing.T) { + testStartTime := time.Now() + + testCases := map[string]struct { + ns string + objs []runtime.Object + args []string + wantOut string + wantOutErr string + wantErr error + }{ + "should print local queue list with all namespaces": { + objs: []runtime.Object{ + utiltesting.MakeLocalQueue("lq1", "ns1"). + ClusterQueue("cq1"). + PendingWorkloads(1). + AdmittedWorkloads(1). + Creation(testStartTime.Add(-1 * time.Hour).Truncate(time.Second)). + Obj(), + utiltesting.MakeLocalQueue("lq2", "ns2"). + ClusterQueue("cq2"). + PendingWorkloads(2). + AdmittedWorkloads(2). + Creation(testStartTime.Add(-2 * time.Hour).Truncate(time.Second)). + Obj(), + }, + args: []string{"--all-namespaces"}, + wantOut: `NAMESPACE NAME CLUSTERQUEUE PENDING WORKLOADS ADMITTED WORKLOADS AGE +ns1 lq1 cq1 1 1 60m +ns2 lq2 cq2 2 2 120m +`, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + streams, _, out, outErr := genericiooptions.NewTestIOStreams() + + tf := cmdtesting.NewTestClientGetter() + if len(tc.ns) > 0 { + tf.WithNamespace(tc.ns) + } else { + tf.WithNamespace(defaultNamespace) + } + + tf.ClientSet = fake.NewSimpleClientset(tc.objs...) + + cmd := NewLocalQueueCmd(tf, streams) + cmd.SetArgs(tc.args) + + gotErr := cmd.Execute() + if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.EquateErrors()); diff != "" { + t.Errorf("Unexpected error (-want/+got)\n%s", diff) + } + + gotOut := out.String() + if diff := cmp.Diff(tc.wantOut, gotOut); diff != "" { + t.Errorf("Unexpected output (-want/+got)\n%s", diff) + } + + gotOutErr := outErr.String() + if diff := cmp.Diff(tc.wantOutErr, gotOutErr); diff != "" { + t.Errorf("Unexpected output (-want/+got)\n%s", diff) + } + }) + } +} diff --git a/cmd/kueuectl/app/options/update_workload_activation_options.go b/cmd/kueuectl/app/options/update_workload_activation_options.go index 8a3cfef0f8..5d07a7cf9e 100644 --- a/cmd/kueuectl/app/options/update_workload_activation_options.go +++ b/cmd/kueuectl/app/options/update_workload_activation_options.go @@ -28,7 +28,6 @@ import ( "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" - "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" "sigs.k8s.io/kueue/cmd/kueuectl/app/util" @@ -59,7 +58,7 @@ func NewUpdateWorkloadActivationOptions(streams genericiooptions.IOStreams, oper } // Complete completes all the required options -func (o *UpdateWorkloadActivationOptions) Complete(clientGetter genericclioptions.RESTClientGetter, cmd *cobra.Command, args []string) error { +func (o *UpdateWorkloadActivationOptions) Complete(clientGetter util.ClientGetter, cmd *cobra.Command, args []string) error { o.Name = args[0] var err error @@ -68,12 +67,7 @@ func (o *UpdateWorkloadActivationOptions) Complete(clientGetter genericclioption return err } - config, err := clientGetter.ToRESTConfig() - if err != nil { - return err - } - - clientset, err := versioned.NewForConfig(config) + clientset, err := clientGetter.KueueClientSet() if err != nil { return err } diff --git a/cmd/kueuectl/app/resume/resume.go b/cmd/kueuectl/app/resume/resume.go index 35b5966f5c..75df6e27bd 100644 --- a/cmd/kueuectl/app/resume/resume.go +++ b/cmd/kueuectl/app/resume/resume.go @@ -18,7 +18,6 @@ package resume import ( "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "sigs.k8s.io/kueue/cmd/kueuectl/app/util" @@ -29,7 +28,7 @@ const ( kueuectl resume workload my-workload` ) -func NewResumeCmd(clientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *cobra.Command { +func NewResumeCmd(clientGetter util.ClientGetter, streams genericiooptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "resume", Short: "Resume the resource", diff --git a/cmd/kueuectl/app/resume/resume_workload.go b/cmd/kueuectl/app/resume/resume_workload.go index 0b966c1998..f0328a4cc5 100644 --- a/cmd/kueuectl/app/resume/resume_workload.go +++ b/cmd/kueuectl/app/resume/resume_workload.go @@ -18,10 +18,10 @@ package resume import ( "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "sigs.k8s.io/kueue/cmd/kueuectl/app/options" + "sigs.k8s.io/kueue/cmd/kueuectl/app/util" ) const ( @@ -30,7 +30,7 @@ const ( kueuectl resume workload my-workload` ) -func NewWorkloadCmd(clientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *cobra.Command { +func NewWorkloadCmd(clientGetter util.ClientGetter, streams genericiooptions.IOStreams) *cobra.Command { o := options.NewUpdateWorkloadActivationOptions(streams, "resumed", true) cmd := &cobra.Command{ diff --git a/cmd/kueuectl/app/stop/stop.go b/cmd/kueuectl/app/stop/stop.go index 268d17e9c3..ff853f94df 100644 --- a/cmd/kueuectl/app/stop/stop.go +++ b/cmd/kueuectl/app/stop/stop.go @@ -18,7 +18,6 @@ package stop import ( "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "sigs.k8s.io/kueue/cmd/kueuectl/app/util" @@ -29,7 +28,7 @@ const ( kueuectl stop workload my-workload` ) -func NewStopCmd(clientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *cobra.Command { +func NewStopCmd(clientGetter util.ClientGetter, streams genericiooptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "stop", Short: "Stop the resource", diff --git a/cmd/kueuectl/app/stop/stop_workload.go b/cmd/kueuectl/app/stop/stop_workload.go index 8dc85bce8a..6e3df4e074 100644 --- a/cmd/kueuectl/app/stop/stop_workload.go +++ b/cmd/kueuectl/app/stop/stop_workload.go @@ -18,10 +18,10 @@ package stop import ( "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "sigs.k8s.io/kueue/cmd/kueuectl/app/options" + "sigs.k8s.io/kueue/cmd/kueuectl/app/util" ) const ( @@ -32,7 +32,7 @@ was preempted (using .spec.active field).` kueuectl stop workload my-workload` ) -func NewWorkloadCmd(clientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *cobra.Command { +func NewWorkloadCmd(clientGetter util.ClientGetter, streams genericiooptions.IOStreams) *cobra.Command { o := options.NewUpdateWorkloadActivationOptions(streams, "stopped", false) cmd := &cobra.Command{ diff --git a/cmd/kueuectl/app/testing/fake.go b/cmd/kueuectl/app/testing/fake.go new file mode 100644 index 0000000000..88839235c1 --- /dev/null +++ b/cmd/kueuectl/app/testing/fake.go @@ -0,0 +1,39 @@ +package testing + +import ( + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/tools/clientcmd" + + "sigs.k8s.io/kueue/client-go/clientset/versioned" + "sigs.k8s.io/kueue/client-go/clientset/versioned/fake" + "sigs.k8s.io/kueue/cmd/kueuectl/app/util" +) + +type TestClientGetter struct { + util.ClientGetter + + ClientSet versioned.Interface + + configFlags *genericclioptions.TestConfigFlags +} + +var _ util.ClientGetter = (*TestClientGetter)(nil) + +func NewTestClientGetter() *TestClientGetter { + clientConfig := &clientcmd.DeferredLoadingClientConfig{} + configFlags := genericclioptions.NewTestConfigFlags().WithClientConfig(clientConfig) + return &TestClientGetter{ + ClientGetter: util.NewClientGetter(configFlags), + ClientSet: fake.NewSimpleClientset(), + configFlags: configFlags, + } +} + +func (f *TestClientGetter) WithNamespace(ns string) *TestClientGetter { + f.configFlags.WithNamespace(ns) + return f +} + +func (f *TestClientGetter) KueueClientSet() (versioned.Interface, error) { + return f.ClientSet, nil +} diff --git a/cmd/kueuectl/app/util/client_getter.go b/cmd/kueuectl/app/util/client_getter.go new file mode 100644 index 0000000000..6ba8e58a13 --- /dev/null +++ b/cmd/kueuectl/app/util/client_getter.go @@ -0,0 +1,39 @@ +package util + +import ( + "k8s.io/cli-runtime/pkg/genericclioptions" + + "sigs.k8s.io/kueue/client-go/clientset/versioned" +) + +type ClientGetter interface { + genericclioptions.RESTClientGetter + + KueueClientSet() (versioned.Interface, error) +} + +type clientGetterImpl struct { + genericclioptions.RESTClientGetter +} + +var _ ClientGetter = (*clientGetterImpl)(nil) + +func NewClientGetter(clientGetter genericclioptions.RESTClientGetter) ClientGetter { + return &clientGetterImpl{ + RESTClientGetter: clientGetter, + } +} + +func (f *clientGetterImpl) KueueClientSet() (versioned.Interface, error) { + config, err := f.ToRESTConfig() + if err != nil { + return nil, err + } + + clientset, err := versioned.NewForConfig(config) + if err != nil { + return nil, err + } + + return clientset, nil +} diff --git a/pkg/util/testing/wrappers.go b/pkg/util/testing/wrappers.go index d56aadeb9b..a610fb0ebd 100644 --- a/pkg/util/testing/wrappers.go +++ b/pkg/util/testing/wrappers.go @@ -517,6 +517,21 @@ func MakeLocalQueue(name, ns string) *LocalQueueWrapper { }} } +// Creation sets the creation timestamp of the LocalQueue. +func (q *LocalQueueWrapper) Creation(t time.Time) *LocalQueueWrapper { + q.CreationTimestamp = metav1.NewTime(t) + return q +} + +// Label sets the label on the LocalQueue. +func (q *LocalQueueWrapper) Label(k, v string) *LocalQueueWrapper { + if q.Labels == nil { + q.Labels = make(map[string]string) + } + q.Labels[k] = v + return q +} + // Obj returns the inner LocalQueue. func (q *LocalQueueWrapper) Obj() *kueue.LocalQueue { return &q.LocalQueue @@ -534,6 +549,12 @@ func (q *LocalQueueWrapper) PendingWorkloads(n int32) *LocalQueueWrapper { return q } +// AdmittedWorkloads updates the admittedWorkloads in status. +func (q *LocalQueueWrapper) AdmittedWorkloads(n int32) *LocalQueueWrapper { + q.Status.AdmittedWorkloads = n + return q +} + // Condition sets a condition on the LocalQueue. func (q *LocalQueueWrapper) Condition(conditionType string, status metav1.ConditionStatus, reason, message string, generation int64) *LocalQueueWrapper { apimeta.SetStatusCondition(&q.Status.Conditions, metav1.Condition{ diff --git a/site/content/en/docs/reference/kubectl-kueue/_index.md b/site/content/en/docs/reference/kubectl-kueue/_index.md index 44f5df3c44..14f70d841f 100644 --- a/site/content/en/docs/reference/kubectl-kueue/_index.md +++ b/site/content/en/docs/reference/kubectl-kueue/_index.md @@ -4,7 +4,7 @@ linkTitle: "Kubectl Kueue Plugin" date: 2024-05-09 weight: 10 description: > - The kubectl-kueue plugin, kueuectl, allows you to create, resume and stop kueue resources such as localqueue and workload. + The kubectl-kueue plugin, kueuectl, allows you to list, create, resume and stop kueue resources such as localqueue and workload. --- ## Syntax diff --git a/site/content/en/docs/reference/kubectl-kueue/commands/list.md b/site/content/en/docs/reference/kubectl-kueue/commands/list.md new file mode 100644 index 0000000000..e477f5b7b8 --- /dev/null +++ b/site/content/en/docs/reference/kubectl-kueue/commands/list.md @@ -0,0 +1,29 @@ +--- +title: "kubectl kueue list" +linkTitle: "List" +date: 2024-05-10 +weight: 10 +description: > + List resource +--- + +### Usage: + +``` +kubectl kueue list [TYPE] +``` + +### Examples: + +```bash +# List workloads +kubectl kueue list localqueue my-local-queue +``` + +## Resource types + +The following table includes a list of all the supported resource types and their abbreviated aliases: + +| Name | Short | API version | Namespaced | Kind | +|------------|-------|------------------------|------------|------------| +| localqueue | lq | kueue.x-k8s.io/v1beta1 | true | LocalQueue | \ No newline at end of file diff --git a/test/integration/kueuectl/list_test.go b/test/integration/kueuectl/list_test.go new file mode 100644 index 0000000000..9be1dfce99 --- /dev/null +++ b/test/integration/kueuectl/list_test.go @@ -0,0 +1,82 @@ +/* +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 kueuectl + +import ( + "fmt" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericiooptions" + + "sigs.k8s.io/kueue/apis/kueue/v1beta1" + "sigs.k8s.io/kueue/cmd/kueuectl/app" + "sigs.k8s.io/kueue/pkg/util/testing" + "sigs.k8s.io/kueue/test/util" +) + +var _ = ginkgo.Describe("Kueuectl List", ginkgo.Ordered, ginkgo.ContinueOnFailure, func() { + var ( + ns *corev1.Namespace + ns1 *corev1.Namespace + cq1 *v1beta1.ClusterQueue + lq1 *v1beta1.LocalQueue + lq2 *v1beta1.LocalQueue + ) + + ginkgo.BeforeEach(func() { + ns1 = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{GenerateName: "ns-"}} + gomega.Expect(k8sClient.Create(ctx, ns1)).To(gomega.Succeed()) + + cq1 = testing.MakeClusterQueue("cq1").Obj() + gomega.Expect(k8sClient.Create(ctx, cq1)).To(gomega.Succeed()) + + lq1 = testing.MakeLocalQueue("lq1", ns1.Name).ClusterQueue(cq1.Name).Obj() + gomega.Expect(k8sClient.Create(ctx, lq1)).To(gomega.Succeed()) + + lq2 = testing.MakeLocalQueue("lq2", ns1.Name).ClusterQueue(cq1.Name).Obj() + gomega.Expect(k8sClient.Create(ctx, lq2)).To(gomega.Succeed()) + }) + + ginkgo.AfterEach(func() { + gomega.Expect(util.DeleteNamespace(ctx, k8sClient, ns)).To(gomega.Succeed()) + gomega.Expect(util.DeleteNamespace(ctx, k8sClient, ns1)).To(gomega.Succeed()) + util.ExpectClusterQueueToBeDeleted(ctx, k8sClient, cq1, true) + }) + + ginkgo.When("List LocalQueue", func() { + // Simple client set that are using on unit tests not allow to filter by field selector. + ginkgo.It("Should print local queue filtered by field selector", func() { + streams, _, output, errOutput := genericiooptions.NewTestIOStreams() + configFlags := CreateConfigFlagsWithRestConfig(cfg, streams) + kueuectl := app.NewKueuectlCmd(app.KueuectlOptions{ConfigFlags: configFlags, IOStreams: streams}) + + kueuectl.SetArgs([]string{"list", "localqueue", "--field-selector", + fmt.Sprintf("metadata.name=%s", lq1.Name), "--namespace", ns1.Name}) + err := kueuectl.Execute() + + gomega.Expect(err).NotTo(gomega.HaveOccurred(), "%s: %s", err, output) + gomega.Expect(errOutput.String()).Should(gomega.BeEmpty()) + gomega.Expect(output.String()).ShouldNot(gomega.ContainSubstring(ns1.Name)) + gomega.Expect(output.String()).Should(gomega.ContainSubstring(cq1.Name)) + gomega.Expect(output.String()).Should(gomega.ContainSubstring(lq1.Name)) + gomega.Expect(output.String()).ShouldNot(gomega.ContainSubstring(lq2.Name)) + }) + }) +})