diff --git a/cmd/cli/vkctl.go b/cmd/cli/vkctl.go index 5e5511d3f2..965220bb6d 100644 --- a/cmd/cli/vkctl.go +++ b/cmd/cli/vkctl.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/util/wait" + "volcano.sh/volcano/pkg/version" ) @@ -45,7 +46,7 @@ func main() { rootCmd.AddCommand(versionCommand()) if err := rootCmd.Execute(); err != nil { - fmt.Printf("Failed to execute command: %v", err) + fmt.Printf("Failed to execute command: %v\n", err) } } diff --git a/pkg/cli/job/delete.go b/pkg/cli/job/delete.go index 6662a2c0fa..7b6eb3be38 100644 --- a/pkg/cli/job/delete.go +++ b/pkg/cli/job/delete.go @@ -39,8 +39,8 @@ var deleteJobFlags = &deleteFlags{} func InitDeleteFlags(cmd *cobra.Command) { initFlags(cmd, &deleteJobFlags.commonFlags) - cmd.Flags().StringVarP(&deleteJobFlags.Namespace, "namespace", "N", "default", "the namespace of job") - cmd.Flags().StringVarP(&deleteJobFlags.JobName, "name", "n", "", "the name of job") + cmd.Flags().StringVarP(&deleteJobFlags.Namespace, "namespace", "n", "default", "the namespace of job") + cmd.Flags().StringVarP(&deleteJobFlags.JobName, "name", "N", "", "the name of job") } // DeleteJob delete the job diff --git a/pkg/cli/job/list.go b/pkg/cli/job/list.go index 41dff254b6..3ef742f1e9 100644 --- a/pkg/cli/job/list.go +++ b/pkg/cli/job/list.go @@ -20,6 +20,7 @@ import ( "fmt" "io" "os" + "strings" "github.com/spf13/cobra" @@ -34,6 +35,8 @@ type listFlags struct { Namespace string SchedulerName string + allNamespace bool + selector string } const ( @@ -74,8 +77,10 @@ var listJobFlags = &listFlags{} func InitListFlags(cmd *cobra.Command) { initFlags(cmd, &listJobFlags.commonFlags) - cmd.Flags().StringVarP(&listJobFlags.Namespace, "namespace", "N", "default", "the namespace of job") + 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") + cmd.Flags().StringVarP(&listJobFlags.selector, "selector", "", "", "fuzzy matching jobName") } // ListJobs lists all jobs details @@ -84,7 +89,9 @@ func ListJobs() error { if err != nil { return err } - + if listJobFlags.allNamespace { + listJobFlags.Namespace = "" + } jobClient := versioned.NewForConfigOrDie(config) jobs, err := jobClient.BatchV1alpha1().Jobs(listJobFlags.Namespace).List(metav1.ListOptions{}) if err != nil { @@ -113,6 +120,9 @@ func PrintJobs(jobs *v1alpha1.JobList, writer io.Writer) { 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 diff --git a/pkg/cli/job/list_test.go b/pkg/cli/job/list_test.go index 64845c70da..21b9f037a4 100644 --- a/pkg/cli/job/list_test.go +++ b/pkg/cli/job/list_test.go @@ -42,20 +42,33 @@ func TestListJob(t *testing.T) { server := httptest.NewServer(handler) defer server.Close() - listJobFlags.Master = server.URL - listJobFlags.Namespace = "test" - testCases := []struct { - Name string - ExpectValue error + Name string + ExpectValue error + AllNamespace bool + Selector string }{ { Name: "ListJob", ExpectValue: nil, }, + { + Name: "ListAllNamespaceJob", + ExpectValue: nil, + AllNamespace: true, + }, } for i, testcase := range testCases { + listJobFlags = &listFlags{ + commonFlags: commonFlags{ + Master: server.URL, + }, + Namespace: "test", + allNamespace: testcase.AllNamespace, + selector: testcase.Selector, + } + err := ListJobs() if err != nil { t.Errorf("case %d (%s): expected: %v, got %v ", i, testcase.Name, testcase.ExpectValue, err) @@ -74,5 +87,11 @@ func TestInitListFlags(t *testing.T) { if cmd.Flag("scheduler") == nil { t.Errorf("Could not find the flag scheduler") } + if cmd.Flag("all-namespaces") == nil { + t.Errorf("Could not find the flag all-namespaces") + } + if cmd.Flag("selector") == nil { + t.Errorf("Could not find the flag selector") + } } diff --git a/pkg/cli/job/resume.go b/pkg/cli/job/resume.go index 54f6a7853a..da573ee1b9 100644 --- a/pkg/cli/job/resume.go +++ b/pkg/cli/job/resume.go @@ -37,8 +37,8 @@ var resumeJobFlags = &resumeFlags{} func InitResumeFlags(cmd *cobra.Command) { initFlags(cmd, &resumeJobFlags.commonFlags) - cmd.Flags().StringVarP(&resumeJobFlags.Namespace, "namespace", "N", "default", "the namespace of job") - cmd.Flags().StringVarP(&resumeJobFlags.JobName, "name", "n", "", "the name of job") + cmd.Flags().StringVarP(&resumeJobFlags.Namespace, "namespace", "n", "default", "the namespace of job") + cmd.Flags().StringVarP(&resumeJobFlags.JobName, "name", "N", "", "the name of job") } // ResumeJob resumes the job diff --git a/pkg/cli/job/run.go b/pkg/cli/job/run.go index 1270aa7bff..c171e15747 100644 --- a/pkg/cli/job/run.go +++ b/pkg/cli/job/run.go @@ -17,10 +17,15 @@ limitations under the License. package job import ( + "fmt" + "io/ioutil" + "strings" + "github.com/spf13/cobra" "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" vkapi "volcano.sh/volcano/pkg/apis/batch/v1alpha1" "volcano.sh/volcano/pkg/client/clientset/versioned" @@ -38,6 +43,7 @@ type runFlags struct { Requests string Limits string SchedulerName string + FileName string } var launchJobFlags = &runFlags{} @@ -47,13 +53,14 @@ func InitRunFlags(cmd *cobra.Command) { initFlags(cmd, &launchJobFlags.commonFlags) cmd.Flags().StringVarP(&launchJobFlags.Image, "image", "i", "busybox", "the container image of job") - cmd.Flags().StringVarP(&launchJobFlags.Namespace, "namespace", "N", "default", "the namespace of job") - cmd.Flags().StringVarP(&launchJobFlags.Name, "name", "n", "test", "the name of job") + cmd.Flags().StringVarP(&launchJobFlags.Namespace, "namespace", "n", "default", "the namespace of job") + cmd.Flags().StringVarP(&launchJobFlags.Name, "name", "N", "test", "the name of job") cmd.Flags().IntVarP(&launchJobFlags.MinAvailable, "min", "m", 1, "the minimal available tasks of job") cmd.Flags().IntVarP(&launchJobFlags.Replicas, "replicas", "r", 1, "the total tasks of job") cmd.Flags().StringVarP(&launchJobFlags.Requests, "requests", "R", "cpu=1000m,memory=100Mi", "the resource request of the task") cmd.Flags().StringVarP(&launchJobFlags.Limits, "limits", "L", "cpu=1000m,memory=100Mi", "the resource limit of the task") - cmd.Flags().StringVarP(&listJobFlags.SchedulerName, "scheduler", "S", "kube-batch", "the scheduler for this job") + cmd.Flags().StringVarP(&launchJobFlags.SchedulerName, "scheduler", "S", "kube-batch", "the scheduler for this job") + cmd.Flags().StringVarP(&launchJobFlags.FileName, "filename", "f", "", "the yaml file of job") } var jobName = "job.volcano.sh" @@ -75,13 +82,57 @@ func RunJob() error { return err } - job := &vkapi.Job{ + job, err := readFile(launchJobFlags.FileName) + if err != nil { + return err + } + + if job == nil { + job = constructLaunchJobFlagsJob(launchJobFlags, req, limit) + } + + jobClient := versioned.NewForConfigOrDie(config) + newJob, err := jobClient.BatchV1alpha1().Jobs(launchJobFlags.Namespace).Create(job) + if err != nil { + return err + } + + fmt.Printf("run job %v successfully\n", newJob.Name) + + return nil +} + +func readFile(filename string) (*vkapi.Job, error) { + if filename == "" { + return nil, nil + } + + if !strings.Contains(filename, ".yaml") && !strings.Contains(filename, ".yml") { + return nil, fmt.Errorf("only support yaml file") + } + + file, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read file, err: %v", err) + } + + var job vkapi.Job + if err := yaml.Unmarshal(file, &job); err != nil { + return nil, fmt.Errorf("Failed to unmarshal file, err: %v", err) + } + + return &job, nil +} + +func constructLaunchJobFlagsJob(launchJobFlags *runFlags, req, limit v1.ResourceList) *vkapi.Job { + return &vkapi.Job{ ObjectMeta: metav1.ObjectMeta{ Name: launchJobFlags.Name, Namespace: launchJobFlags.Namespace, }, Spec: vkapi.JobSpec{ - MinAvailable: int32(launchJobFlags.MinAvailable), + MinAvailable: int32(launchJobFlags.MinAvailable), + SchedulerName: launchJobFlags.SchedulerName, Tasks: []vkapi.TaskSpec{ { Replicas: int32(launchJobFlags.Replicas), @@ -92,7 +143,6 @@ func RunJob() error { Labels: map[string]string{jobName: launchJobFlags.Name}, }, Spec: v1.PodSpec{ - SchedulerName: launchJobFlags.SchedulerName, RestartPolicy: v1.RestartPolicyNever, Containers: []v1.Container{ { @@ -111,11 +161,4 @@ func RunJob() error { }, }, } - - jobClient := versioned.NewForConfigOrDie(config) - if _, err := jobClient.BatchV1alpha1().Jobs(launchJobFlags.Namespace).Create(job); err != nil { - return err - } - - return nil } diff --git a/pkg/cli/job/run_test.go b/pkg/cli/job/run_test.go index 26d6285939..8f743ca7b2 100644 --- a/pkg/cli/job/run_test.go +++ b/pkg/cli/job/run_test.go @@ -19,9 +19,12 @@ package job import ( "encoding/json" "github.com/spf13/cobra" + "io/ioutil" "net/http" "net/http/httptest" + "os" "testing" + "time" v1alpha1 "volcano.sh/volcano/pkg/apis/batch/v1alpha1" ) @@ -41,21 +44,42 @@ func TestCreateJob(t *testing.T) { server := httptest.NewServer(handler) defer server.Close() - launchJobFlags.Master = server.URL - launchJobFlags.Namespace = "test" - launchJobFlags.Requests = "cpu=1000m,memory=100Mi" + fileName := time.Now().String() + "testCreateJob.yaml" + val, err := json.Marshal(response) + if err != nil { + panic(err) + } + err = ioutil.WriteFile(fileName, val, os.ModePerm) + if err != nil { + panic(err) + } + defer os.Remove(fileName) testCases := []struct { Name string ExpectValue error + FileName string }{ { Name: "CreateJob", ExpectValue: nil, }, + { + Name: "CreateJobWithFile", + FileName: fileName, + ExpectValue: nil, + }, } for i, testcase := range testCases { + launchJobFlags = &runFlags{ + commonFlags: commonFlags{ + Master: server.URL, + }, + Namespace: "test", + Requests: "cpu=1000m,memory=100Mi", + } + err := RunJob() if err != nil { t.Errorf("case %d (%s): expected: %v, got %v ", i, testcase.Name, testcase.ExpectValue, err) diff --git a/pkg/cli/job/suspend.go b/pkg/cli/job/suspend.go index 3f6ddcc9b2..f5d2b18cd3 100644 --- a/pkg/cli/job/suspend.go +++ b/pkg/cli/job/suspend.go @@ -36,8 +36,8 @@ var suspendJobFlags = &suspendFlags{} func InitSuspendFlags(cmd *cobra.Command) { initFlags(cmd, &suspendJobFlags.commonFlags) - cmd.Flags().StringVarP(&suspendJobFlags.Namespace, "namespace", "N", "default", "the namespace of job") - cmd.Flags().StringVarP(&suspendJobFlags.JobName, "name", "n", "", "the name of job") + cmd.Flags().StringVarP(&suspendJobFlags.Namespace, "namespace", "n", "default", "the namespace of job") + cmd.Flags().StringVarP(&suspendJobFlags.JobName, "name", "N", "", "the name of job") } // SuspendJob suspends the job diff --git a/pkg/cli/job/util.go b/pkg/cli/job/util.go index 8a5db6a313..db5ee6290e 100644 --- a/pkg/cli/job/util.go +++ b/pkg/cli/job/util.go @@ -20,6 +20,7 @@ import ( "fmt" "os" "strings" + "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -97,3 +98,54 @@ func createJobCommand(config *rest.Config, ns, name string, action vkbatchv1.Act return nil } + +func translateTimestampSince(timestamp metav1.Time) string { + if timestamp.IsZero() { + return "" + } + return HumanDuration(time.Since(timestamp.Time)) +} + +// HumanDuration translate time.Duration to human readable time string +func HumanDuration(d time.Duration) string { + // Allow deviation no more than 2 seconds(excluded) to tolerate machine time + // inconsistence, it can be considered as almost now. + if seconds := int(d.Seconds()); seconds < -1 { + return fmt.Sprintf("") + } else if seconds < 0 { + return fmt.Sprintf("0s") + } else if seconds < 60*2 { + return fmt.Sprintf("%ds", seconds) + } + minutes := int(d / time.Minute) + if minutes < 10 { + s := int(d/time.Second) % 60 + if s == 0 { + return fmt.Sprintf("%dm", minutes) + } + return fmt.Sprintf("%dm%ds", minutes, s) + } else if minutes < 60*3 { + return fmt.Sprintf("%dm", minutes) + } + hours := int(d / time.Hour) + if hours < 8 { + m := int(d/time.Minute) % 60 + if m == 0 { + return fmt.Sprintf("%dh", hours) + } + return fmt.Sprintf("%dh%dm", hours, m) + } else if hours < 48 { + return fmt.Sprintf("%dh", hours) + } else if hours < 24*8 { + h := hours % 24 + if h == 0 { + return fmt.Sprintf("%dd", hours/24) + } + return fmt.Sprintf("%dd%dh", hours/24, h) + } else if hours < 24*365*2 { + return fmt.Sprintf("%dd", hours/24) + } else if hours < 24*365*8 { + return fmt.Sprintf("%dy%dd", hours/24/365, (hours/24)%365) + } + return fmt.Sprintf("%dy", int(hours/24/365)) +} diff --git a/pkg/cli/job/util_test.go b/pkg/cli/job/util_test.go new file mode 100644 index 0000000000..737f56659e --- /dev/null +++ b/pkg/cli/job/util_test.go @@ -0,0 +1,109 @@ +/* +Copyright 2019 The Volcano 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 job + +import ( + "testing" + + "time" +) + +func TestJobUtil(t *testing.T) { + testCases := []struct { + Name string + Duration time.Duration + ExpectValue string + }{ + { + Name: "InvalidTime", + Duration: -time.Minute, + ExpectValue: "", + }, + { + Name: "SmallInvalieTime", + Duration: -time.Millisecond, + ExpectValue: "0s", + }, + { + Name: "NormalSeconds", + Duration: 62 * time.Second, + ExpectValue: "62s", + }, + { + Name: "NormalMinutes", + Duration: 180 * time.Second, + ExpectValue: "3m", + }, + { + Name: "NormalMinutesWithSecond", + Duration: 190 * time.Second, + ExpectValue: "3m10s", + }, + { + Name: "BiggerMinutesWithoutSecond", + Duration: 121*time.Minute + 56*time.Second, + ExpectValue: "121m", + }, + { + Name: "NormalHours", + Duration: 5*time.Hour + 9*time.Second, + ExpectValue: "5h", + }, + { + Name: "NormalHoursWithMinute", + Duration: 5*time.Hour + 7*time.Minute + 9*time.Second, + ExpectValue: "5h7m", + }, + { + Name: "BiggerHoursWithoutMinute", + Duration: 12*time.Hour + 7*time.Minute + 9*time.Second, + ExpectValue: "12h", + }, + { + Name: "NormalDays", + Duration: 5*24*time.Hour + 7*time.Minute + 9*time.Second, + ExpectValue: "5d", + }, + { + Name: "NormalDaysWithHours", + Duration: 5*24*time.Hour + 9*time.Hour, + ExpectValue: "5d9h", + }, + { + Name: "BiggerDayWithoutHours", + Duration: 531*24*time.Hour + 7*time.Minute + 9*time.Second, + ExpectValue: "531d", + }, + { + Name: "NormalYears", + Duration: (365*5+89)*24*time.Hour + 7*time.Minute + 9*time.Second, + ExpectValue: "5y89d", + }, + { + Name: "BiggerYears", + Duration: (365*12+15)*24*time.Hour + 7*time.Minute + 9*time.Second, + ExpectValue: "12y", + }, + } + + for i, testcase := range testCases { + answer := HumanDuration(testcase.Duration) + if answer != testcase.ExpectValue { + t.Errorf("case %d (%s): expected: %v, got %v ", i, testcase.Name, testcase.ExpectValue, answer) + } + } +} diff --git a/pkg/cli/job/view.go b/pkg/cli/job/view.go index 68125b18bf..d8130f3ea2 100644 --- a/pkg/cli/job/view.go +++ b/pkg/cli/job/view.go @@ -17,6 +17,7 @@ limitations under the License. package job import ( + "encoding/json" "fmt" "io" "os" @@ -24,7 +25,10 @@ import ( "github.com/spf13/cobra" + coreV1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "volcano.sh/volcano/pkg/apis/batch/v1alpha1" "volcano.sh/volcano/pkg/client/clientset/versioned" @@ -37,14 +41,21 @@ type viewFlags struct { JobName string } +// level of print indent +const ( + Level0 = iota + Level1 + Level2 +) + var viewJobFlags = &viewFlags{} // InitViewFlags init the view command flags func InitViewFlags(cmd *cobra.Command) { initFlags(cmd, &viewJobFlags.commonFlags) - cmd.Flags().StringVarP(&viewJobFlags.Namespace, "namespace", "N", "default", "the namespace of job") - cmd.Flags().StringVarP(&viewJobFlags.JobName, "name", "n", "", "the name of job") + cmd.Flags().StringVarP(&viewJobFlags.Namespace, "namespace", "n", "default", "the namespace of job") + cmd.Flags().StringVarP(&viewJobFlags.JobName, "name", "N", "", "the name of job") } // ViewJob gives full details of the job @@ -54,7 +65,7 @@ func ViewJob() error { return err } if viewJobFlags.JobName == "" { - err := fmt.Errorf("job name (specified by --name or -n) is mandatory to view a particular job") + err := fmt.Errorf("job name (specified by --name or -N) is mandaorty to view a particular job") return err } @@ -67,35 +78,174 @@ func ViewJob() error { fmt.Printf("No resources found\n") return nil } - PrintJob(job, os.Stdout) - + PrintJobInfo(job, os.Stdout) + PrintEvents(GetEvents(config, job), os.Stdout) return nil } -// PrintJob prints the job details -func PrintJob(job *v1alpha1.Job, writer io.Writer) { - replicas := int32(0) - for _, ts := range job.Spec.Tasks { - replicas += ts.Replicas - } - lines := []string{ - fmt.Sprintf("%s:\t\t%s", Name, job.Name), - fmt.Sprintf("%s:\t%s", Creation, job.CreationTimestamp.Format("2006-01-02 15:04:05")), - fmt.Sprintf("%s:\t%d", Replicas, replicas), - fmt.Sprintf("%s:\t\t%d", Min, job.Status.MinAvailable), - fmt.Sprintf("%s:\t%s", Scheduler, job.Spec.SchedulerName), - "Status", - fmt.Sprintf(" %s:\t%s", Phase, job.Status.State.Phase), - fmt.Sprintf(" %s:\t%d", Version, job.Status.Version), - fmt.Sprintf(" %s:\t%d", RetryCount, job.Status.RetryCount), - fmt.Sprintf(" %s:\t%d", Pending, job.Status.Pending), - fmt.Sprintf(" %s:\t%d", Running, job.Status.Running), - fmt.Sprintf(" %s:\t%d", Succeeded, job.Status.Succeeded), - fmt.Sprintf(" %s:\t%d", Failed, job.Status.Failed), - fmt.Sprintf(" %s:\t%d", Terminating, job.Status.Terminating), - } - _, err := fmt.Fprint(writer, strings.Join(lines, "\n"), "\n") +// PrintJobInfo print the job detailed info into writer +func PrintJobInfo(job *v1alpha1.Job, writer io.Writer) { + WriteLine(writer, Level0, "Name: \t%s\n", job.Name) + WriteLine(writer, Level0, "Namespace: \t%s\n", job.Namespace) + if len(job.Labels) > 0 { + label, _ := json.Marshal(job.Labels) + WriteLine(writer, Level0, "Labels: \t%s\n", string(label)) + } else { + WriteLine(writer, Level0, "Labels: \t\n") + } + if len(job.Annotations) > 0 { + annotation, _ := json.Marshal(job.Annotations) + WriteLine(writer, Level0, "Annotations:\t%s\n", string(annotation)) + } else { + WriteLine(writer, Level0, "Annotations:\t\n") + } + WriteLine(writer, Level0, "API Version:\t%s\n", job.APIVersion) + WriteLine(writer, Level0, "Kind: \t%s\n", job.Kind) + + WriteLine(writer, Level0, "Metadata:\n") + WriteLine(writer, Level1, "Creation Timestamp:\t%s\n", job.CreationTimestamp) + WriteLine(writer, Level1, "Generate Name: \t%s\n", job.GenerateName) + WriteLine(writer, Level1, "Generation: \t%d\n", job.Generation) + WriteLine(writer, Level1, "Resource Version: \t%s\n", job.ResourceVersion) + WriteLine(writer, Level1, "Self Link: \t%s\n", job.SelfLink) + WriteLine(writer, Level1, "UID: \t%s\n", job.UID) + + WriteLine(writer, Level0, "Spec:\n") + WriteLine(writer, Level1, "Min Available: \t%d\n", job.Spec.MinAvailable) + WriteLine(writer, Level1, "Plugins:\n") + WriteLine(writer, Level2, "Env:\t%v\n", job.Spec.Plugins["env"]) + WriteLine(writer, Level2, "Ssh:\t%v\n", job.Spec.Plugins["ssh"]) + WriteLine(writer, Level1, "Scheduler Name: \t%s\n", job.Spec.SchedulerName) + WriteLine(writer, Level1, "Tasks:\n") + for i := 0; i < len(job.Spec.Tasks); i++ { + WriteLine(writer, Level2, "Name:\t%s\n", job.Spec.Tasks[i].Name) + WriteLine(writer, Level2, "Replicas:\t%d\n", job.Spec.Tasks[i].Replicas) + WriteLine(writer, Level2, "Template:\n") + WriteLine(writer, Level2+1, "Metadata:\n") + WriteLine(writer, Level2+2, "Annotations:\n") + WriteLine(writer, Level2+3, "Cri . Cci . Io / Container - Type: \t%s\n", job.Spec.Tasks[i].Template.ObjectMeta.Annotations["cri.cci.io/container-type"]) + WriteLine(writer, Level2+3, "Kubernetes . Io / Availablezone: \t%s\n", job.Spec.Tasks[i].Template.ObjectMeta.Annotations["kubernetes.io/availablezone"]) + WriteLine(writer, Level2+3, "Network . Alpha . Kubernetes . Io / Network:\t%s\n", job.Spec.Tasks[i].Template.ObjectMeta.Annotations["network.alpha.kubernetes.io/network"]) + WriteLine(writer, Level2+2, "Creation Timestamp:\t%s\n", job.Spec.Tasks[i].Template.ObjectMeta.CreationTimestamp) + + WriteLine(writer, Level2+1, "Spec:\n") + WriteLine(writer, Level2+2, "Containers:\n") + for j := 0; j < len(job.Spec.Tasks[i].Template.Spec.Containers); j++ { + WriteLine(writer, Level2+3, "Command:\n") + for k := 0; k < len(job.Spec.Tasks[i].Template.Spec.Containers[j].Command); k++ { + WriteLine(writer, Level2+4, "%s\n", job.Spec.Tasks[i].Template.Spec.Containers[j].Command[k]) + } + WriteLine(writer, Level2+3, "Image:\t%s\n", job.Spec.Tasks[i].Template.Spec.Containers[j].Image) + WriteLine(writer, Level2+3, "Name: \t%s\n", job.Spec.Tasks[i].Template.Spec.Containers[j].Name) + WriteLine(writer, Level2+3, "Ports:\n") + for k := 0; k < len(job.Spec.Tasks[i].Template.Spec.Containers[j].Ports); k++ { + WriteLine(writer, Level2+4, "Container Port:\t%d\n", job.Spec.Tasks[i].Template.Spec.Containers[j].Ports[k].ContainerPort) + WriteLine(writer, Level2+4, "Name: \t%s\n", job.Spec.Tasks[i].Template.Spec.Containers[j].Ports[k].Name) + } + WriteLine(writer, Level2+3, "Resources:\n") + WriteLine(writer, Level2+4, "Limits:\n") + WriteLine(writer, Level2+5, "Cpu: \t%s\n", job.Spec.Tasks[i].Template.Spec.Containers[j].Resources.Limits.Cpu()) + WriteLine(writer, Level2+5, "Memory:\t%s\n", job.Spec.Tasks[i].Template.Spec.Containers[j].Resources.Limits.Memory()) + WriteLine(writer, Level2+4, "Requests:\n") + WriteLine(writer, Level2+5, "Cpu: \t%s\n", job.Spec.Tasks[i].Template.Spec.Containers[j].Resources.Requests.Cpu()) + WriteLine(writer, Level2+5, "Memory:\t%s\n", job.Spec.Tasks[i].Template.Spec.Containers[j].Resources.Requests.Memory()) + WriteLine(writer, Level2+4, "Working Dir:\t%s\n", job.Spec.Tasks[i].Template.Spec.Containers[j].WorkingDir) + } + WriteLine(writer, Level2+2, "Image Pull Secrets:\n") + for j := 0; j < len(job.Spec.Tasks[i].Template.Spec.ImagePullSecrets); j++ { + WriteLine(writer, Level2+3, "Name: \t%s\n", job.Spec.Tasks[i].Template.Spec.ImagePullSecrets[j].Name) + } + WriteLine(writer, Level2+2, "Restart Policy: \t%s\n", job.Spec.Tasks[i].Template.Spec.RestartPolicy) + } + + WriteLine(writer, Level0, "Status:\n") + if job.Status.Succeeded > 0 { + WriteLine(writer, Level1, "Succeeded: \t%d\n", job.Status.Succeeded) + } + if job.Status.Pending > 0 { + WriteLine(writer, Level1, "Pending: \t%d\n", job.Status.Pending) + } + if job.Status.Running > 0 { + WriteLine(writer, Level1, "Running: \t%d\n", job.Status.Running) + } + if job.Status.Failed > 0 { + WriteLine(writer, Level1, "Failed: \t%d\n", job.Status.Failed) + } + if job.Status.Terminating > 0 { + WriteLine(writer, Level1, "Terminating: \t%d\n", job.Status.Terminating) + } + if job.Status.RetryCount > 0 { + WriteLine(writer, Level1, "RetryCount: \t%d\n", job.Status.RetryCount) + } + if job.Status.MinAvailable > 0 { + WriteLine(writer, Level1, "Min Available:\t%d\n", job.Status.MinAvailable) + } + if job.Status.Version > 0 { + WriteLine(writer, Level1, "Version: \t%d\n", job.Status.Version) + } + + WriteLine(writer, Level1, "State:\n") + WriteLine(writer, Level2, "Phase:\t%s\n", job.Status.State.Phase) + if len(job.Status.ControlledResources) > 0 { + WriteLine(writer, Level1, "Controlled Resources:\n") + for key, value := range job.Status.ControlledResources { + WriteLine(writer, Level2, "%s: \t%s\n", key, value) + } + } +} + +// PrintEvents print event info to writer +func PrintEvents(events []coreV1.Event, writer io.Writer) { + if len(events) > 0 { + WriteLine(writer, Level0, "%s:\n%-15s\t%-40s\t%-30s\t%-40s\t%s\n", "Events", "Type", "Reason", "Age", "Form", "Message") + WriteLine(writer, Level0, "%-15s\t%-40s\t%-30s\t%-40s\t%s\n", "-------", "-------", "-------", "-------", "-------") + for _, e := range events { + var interval string + if e.Count > 1 { + interval = fmt.Sprintf("%s (x%d over %s)", translateTimestampSince(e.LastTimestamp), e.Count, translateTimestampSince(e.FirstTimestamp)) + } else { + interval = translateTimestampSince(e.FirstTimestamp) + } + EventSourceString := []string{e.Source.Component} + if len(e.Source.Host) > 0 { + EventSourceString = append(EventSourceString, e.Source.Host) + } + WriteLine(writer, Level0, "%-15v\t%-40v\t%-30s\t%-40s\t%v\n", + e.Type, + e.Reason, + interval, + strings.Join(EventSourceString, ", "), + strings.TrimSpace(e.Message), + ) + } + } else { + WriteLine(writer, Level0, "Events: \t\n") + } + +} + +// GetEvents get the job event by config +func GetEvents(config *rest.Config, job *v1alpha1.Job) []coreV1.Event { + kubernetes, err := kubernetes.NewForConfig(config) if err != nil { - fmt.Printf("Failed to print view command result: %s.\n", err) + fmt.Printf("%v\n", err) + return nil + } + events, _ := kubernetes.CoreV1().Events(viewJobFlags.Namespace).List(metav1.ListOptions{}) + var jobEvents []coreV1.Event + for _, v := range events.Items { + if strings.HasPrefix(v.ObjectMeta.Name, job.Name+".") { + jobEvents = append(jobEvents, v) + } + } + return jobEvents +} + +// WriteLine write lines with specified indent +func WriteLine(writer io.Writer, spaces int, content string, params ...interface{}) { + prefix := "" + for i := 0; i < spaces; i++ { + prefix += " " } + fmt.Fprintf(writer, prefix+content, params...) } diff --git a/pkg/cli/job/view_test.go b/pkg/cli/job/view_test.go index 564da3f148..f199ec1001 100644 --- a/pkg/cli/job/view_test.go +++ b/pkg/cli/job/view_test.go @@ -18,24 +18,108 @@ package job import ( "encoding/json" - "github.com/spf13/cobra" + "math" "net/http" "net/http/httptest" + "strings" "testing" + "github.com/spf13/cobra" + + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1alpha1 "volcano.sh/volcano/pkg/apis/batch/v1alpha1" ) func TestViewJob(t *testing.T) { - response := v1alpha1.Job{} - response.Name = "testJob" + response := v1alpha1.Job{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "testJobWithLongLongLongName", + Labels: map[string]string{ + "LabelWithLongLongLongLongName": "LongLongLongLongLabelValue", + }, + Annotations: map[string]string{ + "AnnotationWithLongLongLongLongName": "LongLongLongLongAnnotationValue", + }, + }, + Spec: v1alpha1.JobSpec{ + Tasks: []v1alpha1.TaskSpec{ + { + Name: "taskWithLongLongLongLongName", + Replicas: math.MaxInt32, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{}, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Command: []string{"echo", "123"}, + Ports: []v1.ContainerPort{ + { + Name: "placeholder", + }, + }, + }, + }, + ImagePullSecrets: []v1.LocalObjectReference{ + { + Name: "imagepull-secret", + }, + }, + }, + }, + }, + }, + }, + Status: v1alpha1.JobStatus{ + Succeeded: 1, + Pending: 3, + Running: 1, + Failed: 2, + Terminating: 4, + RetryCount: 5, + MinAvailable: 6, + Version: 7, + ControlledResources: map[string]string{ + "svc": "", + }, + }, + } + + eventList := v1.EventList{ + Items: []v1.Event{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: response.Name + ".123", + }, + Count: 1, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: response.Name + ".456", + }, + Count: 2, + FirstTimestamp: metav1.Now(), + }, + }, + } handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - val, err := json.Marshal(response) + + if strings.Contains(r.URL.String(), "job") { + val, err := json.Marshal(response) + if err == nil { + w.Write(val) + } + return + } + + val, err := json.Marshal(eventList) if err == nil { w.Write(val) } + return })