From dc5526efa4f7c729eef07d630019b9dab77aeada Mon Sep 17 00:00:00 2001 From: googs1025 Date: Sat, 15 Jun 2024 09:27:06 +0800 Subject: [PATCH] feat: add vcctl job list queueName filter Signed-off-by: googs1025 --- pkg/cli/job/list.go | 45 ++++- pkg/cli/job/list_test.go | 254 ++++++++++++++++++++---- pkg/cli/jobtemplate/jobtemplate_test.go | 65 ++---- pkg/cli/util/util.go | 35 ++++ test/e2e/vcctl/vcctl.go | 1 + 5 files changed, 302 insertions(+), 98 deletions(-) diff --git a/pkg/cli/job/list.go b/pkg/cli/job/list.go index 258a7c13a4f..18b4242e6ba 100644 --- a/pkg/cli/job/list.go +++ b/pkg/cli/job/list.go @@ -35,6 +35,7 @@ import ( type listFlags struct { util.CommonFlags + QueueName string Namespace string SchedulerName string allNamespace bool @@ -83,6 +84,7 @@ var listJobFlags = &listFlags{} func InitListFlags(cmd *cobra.Command) { util.InitFlags(cmd, &listJobFlags.CommonFlags) + cmd.Flags().StringVarP(&listJobFlags.QueueName, "queue", "q", "", "list job with specified queue name") cmd.Flags().StringVarP(&listJobFlags.Namespace, "namespace", "n", "default", "the namespace of job") cmd.Flags().StringVarP(&listJobFlags.SchedulerName, "scheduler", "S", "", "list job with specified scheduler name") cmd.Flags().BoolVarP(&listJobFlags.allNamespace, "all-namespaces", "", false, "list jobs in all namespaces") @@ -104,11 +106,33 @@ func ListJobs(ctx context.Context) error { return err } - if len(jobs.Items) == 0 { + // define the filter callback function based on different flags + filterFunc := func(job v1alpha1.Job) bool { + // filter by QueueName if specified + if listJobFlags.QueueName != "" && listJobFlags.QueueName != job.Spec.Queue { + return false + } + // filter by SchedulerName if specified + if listJobFlags.SchedulerName != "" && listJobFlags.SchedulerName != job.Spec.SchedulerName { + return false + } + // filter by selector if specified + if listJobFlags.selector != "" && !strings.Contains(job.Name, listJobFlags.selector) { + return false + } + // filter by namespace if specified + if listJobFlags.Namespace != "" && listJobFlags.Namespace != job.Namespace { + return false + } + return true + } + filteredJobs := filterJobs(jobs, filterFunc) + + if len(filteredJobs.Items) == 0 { fmt.Printf("No resources found\n") return nil } - PrintJobs(jobs, os.Stdout) + PrintJobs(filteredJobs, os.Stdout) return nil } @@ -133,12 +157,6 @@ func PrintJobs(jobs *v1alpha1.JobList, writer io.Writer) { } for _, job := range jobs.Items { - if listJobFlags.SchedulerName != "" && listJobFlags.SchedulerName != job.Spec.SchedulerName { - continue - } - if !strings.Contains(job.Name, listJobFlags.selector) { - continue - } replicas := int32(0) for _, ts := range job.Spec.Tasks { replicas += ts.Replicas @@ -177,3 +195,14 @@ func getMaxLen(jobs *v1alpha1.JobList) []int { return []int{maxNameLen + 3, maxNamespaceLen + 3} } + +// filterJobs filters jobs based on the provided filter callback function. +func filterJobs(jobs *v1alpha1.JobList, filterFunc func(job v1alpha1.Job) bool) *v1alpha1.JobList { + filteredJobs := &v1alpha1.JobList{} + for _, job := range jobs.Items { + if filterFunc(job) { + filteredJobs.Items = append(filteredJobs.Items, job) + } + } + return filteredJobs +} diff --git a/pkg/cli/job/list_test.go b/pkg/cli/job/list_test.go index 59710655a86..275a192d118 100644 --- a/pkg/cli/job/list_test.go +++ b/pkg/cli/job/list_test.go @@ -18,66 +18,236 @@ package job import ( "context" - "encoding/json" - "net/http" - "net/http/httptest" + "reflect" "testing" - "volcano.sh/volcano/pkg/cli/util" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/spf13/cobra" "volcano.sh/apis/pkg/apis/batch/v1alpha1" + "volcano.sh/volcano/pkg/cli/util" ) func TestListJob(t *testing.T) { - response := v1alpha1.JobList{} - response.Items = append(response.Items, v1alpha1.Job{}) - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - val, err := json.Marshal(response) - if err == nil { - w.Write(val) - } - }) - - server := httptest.NewServer(handler) - defer server.Close() + var ( + namespaceFilter = "kube-system" + schedulerFilter = "volcano" + queueFilter = "test-queue" + selectorFilter = "test-job" + ) testCases := []struct { - Name string - ExpectValue error - AllNamespace bool - Selector string + Name string + Response interface{} + AllNamespace bool + Scheduler string + Selector string + QueueName string + Namespace string + ExpectedErr error + ExpectedOutput string }{ { - Name: "ListJob", - ExpectValue: nil, + Name: "Normal Case", + Response: &v1alpha1.JobList{ + Items: []v1alpha1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + Namespace: "default", + }, + Spec: v1alpha1.JobSpec{ + Queue: "default", + }, + }, + }, + }, + ExpectedErr: nil, + ExpectedOutput: `Name Creation Phase JobType Replicas Min Pending Running Succeeded Failed Unknown RetryCount +test-job 0001-01-01 Batch 0 0 0 0 0 0 0 0`, }, { - Name: "ListAllNamespaceJob", - ExpectValue: nil, + Name: "Normal Case with queueName filter", + QueueName: queueFilter, + Response: &v1alpha1.JobList{ + Items: []v1alpha1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + Namespace: "default", + }, + Spec: v1alpha1.JobSpec{ + Queue: "default", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-queue", + Namespace: "default", + }, + Spec: v1alpha1.JobSpec{ + Queue: queueFilter, + }, + }, + }, + }, + ExpectedErr: nil, + ExpectedOutput: `Name Creation Phase JobType Replicas Min Pending Running Succeeded Failed Unknown RetryCount +test-queue 0001-01-01 Batch 0 0 0 0 0 0 0 0`, + }, + { + Name: "Normal Case with namespace filter", + Namespace: namespaceFilter, + Response: &v1alpha1.JobList{ + Items: []v1alpha1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + Namespace: namespaceFilter, + }, + Spec: v1alpha1.JobSpec{ + Queue: "default", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-queue", + Namespace: "default", + }, + Spec: v1alpha1.JobSpec{ + Queue: "test-queue", + }, + }, + }, + }, + ExpectedErr: nil, + ExpectedOutput: `Name Creation Phase JobType Replicas Min Pending Running Succeeded Failed Unknown RetryCount +test-job 0001-01-01 Batch 0 0 0 0 0 0 0 0`, + }, + { + Name: "Normal Case with all namespace filter", AllNamespace: true, + Response: &v1alpha1.JobList{ + Items: []v1alpha1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + Namespace: "kube-sysyem", + }, + Spec: v1alpha1.JobSpec{ + Queue: "default", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-queue", + Namespace: "default", + }, + Spec: v1alpha1.JobSpec{ + Queue: "test-queue", + }, + }, + }, + }, + ExpectedErr: nil, + ExpectedOutput: `Namespace Name Creation Phase JobType Replicas Min Pending Running Succeeded Failed Unknown RetryCount +kube-sysyem test-job 0001-01-01 Batch 0 0 0 0 0 0 0 0 +default test-queue 0001-01-01 Batch 0 0 0 0 0 0 0 0`, }, - } - - for i, testcase := range testCases { - listJobFlags = &listFlags{ - CommonFlags: util.CommonFlags{ - Master: server.URL, + { + Name: "Normal Case with scheduler filter", + Scheduler: schedulerFilter, + Response: &v1alpha1.JobList{ + Items: []v1alpha1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-job", + Namespace: "kube-sysyem", + }, + Spec: v1alpha1.JobSpec{ + Queue: "default", + SchedulerName: "test-scheduler", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-queue", + Namespace: "default", + }, + Spec: v1alpha1.JobSpec{ + Queue: "test-queue", + SchedulerName: schedulerFilter, + }, + }, + }, + }, + ExpectedErr: nil, + ExpectedOutput: `Name Creation Phase JobType Replicas Min Pending Running Succeeded Failed Unknown RetryCount +test-queue 0001-01-01 Batch 0 0 0 0 0 0 0 0`, + }, + { + Name: "Normal Case with selector filter", + Selector: selectorFilter, + Response: &v1alpha1.JobList{ + Items: []v1alpha1.Job{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: selectorFilter, + Namespace: "kube-sysyem", + }, + Spec: v1alpha1.JobSpec{ + Queue: "default", + SchedulerName: "test-scheduler", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-queue", + Namespace: "default", + }, + Spec: v1alpha1.JobSpec{ + Queue: "test-queue", + SchedulerName: schedulerFilter, + }, + }, + }, }, - Namespace: "test", - allNamespace: testcase.AllNamespace, - selector: testcase.Selector, - } - - err := ListJobs(context.TODO()) - if err != nil { - t.Errorf("case %d (%s): expected: %v, got %v ", i, testcase.Name, testcase.ExpectValue, err) - } + ExpectedErr: nil, + ExpectedOutput: `Name Creation Phase JobType Replicas Min Pending Running Succeeded Failed Unknown RetryCount +test-job 0001-01-01 Batch 0 0 0 0 0 0 0 0`, + }, } + for _, testcase := range testCases { + t.Run(testcase.Name, func(t *testing.T) { + + server := util.CreateTestServer(testcase.Response) + defer server.Close() + + listJobFlags = &listFlags{ + CommonFlags: util.CommonFlags{ + Master: server.URL, + }, + Namespace: testcase.Namespace, + allNamespace: testcase.AllNamespace, + selector: testcase.Selector, + SchedulerName: testcase.Scheduler, + QueueName: testcase.QueueName, + } + r, oldStdout := util.RedirectStdout() + defer r.Close() + err := ListJobs(context.TODO()) + gotOutput := util.CaptureOutput(r, oldStdout) + if !reflect.DeepEqual(err, testcase.ExpectedErr) { + t.Fatalf("test case: %s failed: got: %v, want: %v", testcase.Name, err, testcase.ExpectedErr) + } + if gotOutput != testcase.ExpectedOutput { + t.Errorf("test case: %s failed: got: %s, want: %s", testcase.Name, gotOutput, testcase.ExpectedOutput) + } + }) + } } func TestInitListFlags(t *testing.T) { @@ -96,5 +266,7 @@ func TestInitListFlags(t *testing.T) { if cmd.Flag("selector") == nil { t.Errorf("Could not find the flag selector") } - + if cmd.Flag("queue") == nil { + t.Errorf("Could not find the flag queue") + } } diff --git a/pkg/cli/jobtemplate/jobtemplate_test.go b/pkg/cli/jobtemplate/jobtemplate_test.go index 3758c34ec02..a9b565ff9bf 100644 --- a/pkg/cli/jobtemplate/jobtemplate_test.go +++ b/pkg/cli/jobtemplate/jobtemplate_test.go @@ -2,14 +2,10 @@ package jobtemplate import ( "context" - "encoding/json" "fmt" "io" - "net/http" - "net/http/httptest" "os" "reflect" - "strings" "testing" "github.com/spf13/cobra" @@ -17,6 +13,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" flowv1alpha1 "volcano.sh/apis/pkg/apis/flow/v1alpha1" + "volcano.sh/volcano/pkg/cli/util" ) func TestListJobTemplate(t *testing.T) { @@ -47,16 +44,16 @@ test-jobtemplate default`, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - server := createTestServer(testCase.Response) + server := util.CreateTestServer(testCase.Response) defer server.Close() // Set the server URL as the master flag listJobTemplateFlags.Master = server.URL listJobTemplateFlags.Namespace = testCase.Namespace - r, oldStdout := redirectStdout() + r, oldStdout := util.RedirectStdout() defer r.Close() err := ListJobTemplate(context.TODO()) - gotOutput := captureOutput(r, oldStdout) + gotOutput := util.CaptureOutput(r, oldStdout) if !reflect.DeepEqual(err, testCase.ExpectedErr) { t.Fatalf("test case: %s failed: got: %v, want: %v", testCase.name, err, testCase.ExpectedErr) @@ -94,7 +91,7 @@ test-jobtemplate default`, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - server := createTestServer(testCase.Response) + server := util.CreateTestServer(testCase.Response) defer server.Close() // Set the server URL as the master flag getJobTemplateFlags.Master = server.URL @@ -102,10 +99,10 @@ test-jobtemplate default`, getJobTemplateFlags.Namespace = testCase.Namespace getJobTemplateFlags.Name = testCase.Name - r, oldStdout := redirectStdout() + r, oldStdout := util.RedirectStdout() defer r.Close() err := GetJobTemplate(context.TODO()) - gotOutput := captureOutput(r, oldStdout) + gotOutput := util.CaptureOutput(r, oldStdout) if !reflect.DeepEqual(err, testCase.ExpectedErr) { t.Fatalf("test case: %s failed: got: %v, want: %v", testCase.name, err, testCase.ExpectedErr) } @@ -156,7 +153,7 @@ Deleted JobTemplate: default/b`, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - server := createTestServer(testCase.Response) + server := util.CreateTestServer(testCase.Response) defer server.Close() // Set the server URL as the master flag deleteJobTemplateFlags.Master = server.URL @@ -178,10 +175,10 @@ Deleted JobTemplate: default/b`, }() } - r, oldStdout := redirectStdout() + r, oldStdout := util.RedirectStdout() defer r.Close() err := DeleteJobTemplate(context.TODO()) - gotOutput := captureOutput(r, oldStdout) + gotOutput := util.CaptureOutput(r, oldStdout) if !reflect.DeepEqual(err, testCase.ExpectedErr) { t.Fatalf("test case: %s failed: got: %v, want: %v", testCase.name, err, testCase.ExpectedErr) } @@ -216,7 +213,7 @@ Created JobTemplate: default/b`, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - server := createTestServer(testCase.Response) + server := util.CreateTestServer(testCase.Response) defer server.Close() // Set the server URL as the master flag createJobTemplateFlags.Master = server.URL @@ -235,10 +232,10 @@ Created JobTemplate: default/b`, } }() } - r, oldStdout := redirectStdout() + r, oldStdout := util.RedirectStdout() defer r.Close() err := CreateJobTemplate(context.TODO()) - gotOutput := captureOutput(r, oldStdout) + gotOutput := util.CaptureOutput(r, oldStdout) if !reflect.DeepEqual(err, testCase.ExpectedErr) { t.Fatalf("test case: %s failed: got: %v, want: %v", testCase.name, err, testCase.ExpectedErr) } @@ -306,7 +303,7 @@ status: {} } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - server := createTestServer(testCase.Response) + server := util.CreateTestServer(testCase.Response) defer server.Close() // Set the server URL as the master flag describeJobTemplateFlags.Master = server.URL @@ -314,10 +311,10 @@ status: {} describeJobTemplateFlags.Name = testCase.name describeJobTemplateFlags.Format = testCase.Format - r, oldStdout := redirectStdout() + r, oldStdout := util.RedirectStdout() defer r.Close() err := DescribeJobTemplate(context.TODO()) - gotOutput := captureOutput(r, oldStdout) + gotOutput := util.CaptureOutput(r, oldStdout) if !reflect.DeepEqual(err, testCase.ExpectedErr) { t.Fatalf("test case: %s failed: got: %v, want: %v", testCase.name, err, testCase.ExpectedErr) } @@ -328,36 +325,6 @@ status: {} } } -func createTestServer(response interface{}) *httptest.Server { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - val, err := json.Marshal(response) - if err == nil { - w.Write(val) - } - }) - - server := httptest.NewServer(handler) - return server -} - -// redirectStdout redirects os.Stdout to a pipe and returns the read and write ends of the pipe. -func redirectStdout() (*os.File, *os.File) { - r, w, _ := os.Pipe() - oldStdout := os.Stdout - os.Stdout = w - return r, oldStdout -} - -// captureOutput reads from r until EOF and returns the result as a string. -func captureOutput(r *os.File, oldStdout *os.File) string { - w := os.Stdout - os.Stdout = oldStdout - w.Close() - gotOutput, _ := io.ReadAll(r) - return strings.TrimSpace(string(gotOutput)) -} - func createAndWriteFile(filePath, content string) error { if _, err := os.Stat(filePath); os.IsNotExist(err) { file, err := os.Create(filePath) diff --git a/pkg/cli/util/util.go b/pkg/cli/util/util.go index c435ce77c76..b81f48864c3 100644 --- a/pkg/cli/util/util.go +++ b/pkg/cli/util/util.go @@ -18,7 +18,11 @@ package util import ( "context" + "encoding/json" "fmt" + "io" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -200,3 +204,34 @@ func HumanDuration(d time.Duration) string { } return fmt.Sprintf("%dy", hours/24/365) } + +// RedirectStdout redirects os.Stdout to a pipe and returns the read and write ends of the pipe. +func RedirectStdout() (*os.File, *os.File) { + r, w, _ := os.Pipe() + oldStdout := os.Stdout + os.Stdout = w + return r, oldStdout +} + +// CaptureOutput reads from r until EOF and returns the result as a string. +func CaptureOutput(r *os.File, oldStdout *os.File) string { + w := os.Stdout + os.Stdout = oldStdout + w.Close() + gotOutput, _ := io.ReadAll(r) + return strings.TrimSpace(string(gotOutput)) +} + +// CreateTestServer creates an HTTP server that responds with the given response. +func CreateTestServer(response interface{}) *httptest.Server { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + val, err := json.Marshal(response) + if err == nil { + w.Write(val) + } + }) + + server := httptest.NewServer(handler) + return server +} diff --git a/test/e2e/vcctl/vcctl.go b/test/e2e/vcctl/vcctl.go index 07d336849f5..a3bd354461c 100644 --- a/test/e2e/vcctl/vcctl.go +++ b/test/e2e/vcctl/vcctl.go @@ -99,6 +99,7 @@ Flags: -k, --kubeconfig string (optional) absolute path to the kubeconfig file (default "` + kubeConfig + `") -s, --master string the address of apiserver -n, --namespace string the namespace of job (default "default") + -q, --queue string list job with specified queue name -S, --scheduler string list job with specified scheduler name --selector string fuzzy matching jobName