diff --git a/Makefile b/Makefile index 74c4177994..51a355cb55 100644 --- a/Makefile +++ b/Makefile @@ -96,3 +96,5 @@ command-lines: go build -ldflags ${LD_FLAGS} -o=${BIN_DIR}/vcancel ./cmd/cli/vcancel go build -ldflags ${LD_FLAGS} -o=${BIN_DIR}/vresume ./cmd/cli/vresume go build -ldflags ${LD_FLAGS} -o=${BIN_DIR}/vsuspend ./cmd/cli/vsuspend + go build -ldflags ${LD_FLAGS} -o=${BIN_DIR}/vjobs ./cmd/cli/vjobs + go build -ldflags ${LD_FLAGS} -o=${BIN_DIR}/vqueues ./cmd/cli/vqueues diff --git a/cmd/cli/util/util.go b/cmd/cli/util/util.go index 73ef622573..7b464701e9 100644 --- a/cmd/cli/util/util.go +++ b/cmd/cli/util/util.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Volcano Authors. +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. diff --git a/cmd/cli/vcancel/main.go b/cmd/cli/vcancel/main.go index 302adb94c5..939aab3712 100644 --- a/cmd/cli/vcancel/main.go +++ b/cmd/cli/vcancel/main.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Volcano Authors. +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. @@ -27,7 +27,7 @@ import ( "k8s.io/klog" "volcano.sh/volcano/cmd/cli/util" - "volcano.sh/volcano/pkg/cli/job" + "volcano.sh/volcano/pkg/cli/vcancel" ) var logFlushFreq = pflag.Duration("log-flush-frequency", 5*time.Second, "Maximum number of seconds between log flushes") @@ -45,14 +45,14 @@ func main() { Short: "cancel a job", Long: `cancel a running, pending, or aborted job with specified name in default or specified namespace`, Run: func(cmd *cobra.Command, args []string) { - util.CheckError(cmd, job.DeleteJob()) + util.CheckError(cmd, vcancel.CancelJob()) }, } jobCancelCmd := &rootCmd - job.InitDeleteFlags(jobCancelCmd) + vcancel.InitCancelFlags(jobCancelCmd) if err := rootCmd.Execute(); err != nil { - fmt.Printf("Failed to execute command: %v\n", err) + fmt.Printf("Failed to execute vcancel: %v\n", err) os.Exit(-2) } } diff --git a/cmd/cli/vjobs/main.go b/cmd/cli/vjobs/main.go new file mode 100644 index 0000000000..e942788a6f --- /dev/null +++ b/cmd/cli/vjobs/main.go @@ -0,0 +1,58 @@ +/* +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 main + +import ( + "fmt" + "os" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/klog" + + "volcano.sh/volcano/cmd/cli/util" + "volcano.sh/volcano/pkg/cli/vjobs" +) + +var logFlushFreq = pflag.Duration("log-flush-frequency", 5*time.Second, "Maximum number of seconds between log flushes") + +func main() { + // flag.InitFlags() + klog.InitFlags(nil) + + // The default klog flush interval is 30 seconds, which is frighteningly long. + go wait.Until(klog.Flush, *logFlushFreq, wait.NeverStop) + defer klog.Flush() + + rootCmd := cobra.Command{ + Use: "vjobs", + Short: "view job information", + Long: `view information of a job with specified name or jobs from the same namespace`, + Run: func(cmd *cobra.Command, args []string) { + util.CheckError(cmd, vjobs.ViewJob()) + }, + } + + jobViewCmd := &rootCmd + vjobs.InitViewFlags(jobViewCmd) + if err := rootCmd.Execute(); err != nil { + fmt.Printf("Failed to execute vjobs: %v\n", err) + os.Exit(-2) + } +} diff --git a/cmd/cli/vqueues/main.go b/cmd/cli/vqueues/main.go new file mode 100644 index 0000000000..43c3e25a60 --- /dev/null +++ b/cmd/cli/vqueues/main.go @@ -0,0 +1,58 @@ +/* +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 main + +import ( + "fmt" + "os" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/klog" + + "volcano.sh/volcano/cmd/cli/util" + "volcano.sh/volcano/pkg/cli/vqueues" +) + +var logFlushFreq = pflag.Duration("log-flush-frequency", 5*time.Second, "Maximum number of seconds between log flushes") + +func main() { + // flag.InitFlags() + klog.InitFlags(nil) + + // The default klog flush interval is 30 seconds, which is frighteningly long. + go wait.Until(klog.Flush, *logFlushFreq, wait.NeverStop) + defer klog.Flush() + + rootCmd := cobra.Command{ + Use: "vqueues", + Short: "view queue information", + Long: `view information of a queue with specified name or queues from the same namespace`, + Run: func(cmd *cobra.Command, args []string) { + util.CheckError(cmd, vqueues.GetQueue()) + }, + } + + jobGetCmd := &rootCmd + vqueues.InitGetFlags(jobGetCmd) + if err := rootCmd.Execute(); err != nil { + fmt.Printf("Failed to execute vqueues: %v\n", err) + os.Exit(-2) + } +} diff --git a/cmd/cli/vresume/main.go b/cmd/cli/vresume/main.go index f520270f5c..5de6f6d2d6 100644 --- a/cmd/cli/vresume/main.go +++ b/cmd/cli/vresume/main.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Volcano Authors. +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. @@ -27,7 +27,7 @@ import ( "k8s.io/klog" "volcano.sh/volcano/cmd/cli/util" - "volcano.sh/volcano/pkg/cli/job" + "volcano.sh/volcano/pkg/cli/vresume" ) var logFlushFreq = pflag.Duration("log-flush-frequency", 5*time.Second, "Maximum number of seconds between log flushes") @@ -45,14 +45,14 @@ func main() { Short: "resume a job", Long: `resume an aborted job with specified name in default or specified namespace`, Run: func(cmd *cobra.Command, args []string) { - util.CheckError(cmd, job.ResumeJob()) + util.CheckError(cmd, vresume.ResumeJob()) }, } jobResumeCmd := &rootCmd - job.InitResumeFlags(jobResumeCmd) + vresume.InitResumeFlags(jobResumeCmd) if err := rootCmd.Execute(); err != nil { - fmt.Printf("Failed to execute command: %v\n", err) + fmt.Printf("Failed to execute vresume: %v\n", err) os.Exit(-2) } } diff --git a/cmd/cli/vsuspend/main.go b/cmd/cli/vsuspend/main.go index 9bcff6308b..edea6c9880 100644 --- a/cmd/cli/vsuspend/main.go +++ b/cmd/cli/vsuspend/main.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Volcano Authors. +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. @@ -27,7 +27,7 @@ import ( "k8s.io/klog" "volcano.sh/volcano/cmd/cli/util" - "volcano.sh/volcano/pkg/cli/job" + "volcano.sh/volcano/pkg/cli/vsuspend" ) var logFlushFreq = pflag.Duration("log-flush-frequency", 5*time.Second, "Maximum number of seconds between log flushes") @@ -45,14 +45,14 @@ func main() { Short: "suspend a job", Long: `suspend a running or pending job with specified name in default or specified namespace`, Run: func(cmd *cobra.Command, args []string) { - util.CheckError(cmd, job.SuspendJob()) + util.CheckError(cmd, vsuspend.SuspendJob()) }, } jobSuspendCmd := &rootCmd - job.InitSuspendFlags(jobSuspendCmd) + vsuspend.InitSuspendFlags(jobSuspendCmd) if err := rootCmd.Execute(); err != nil { - fmt.Printf("Failed to execute command: %v\n", err) + fmt.Printf("Failed to execute vsuspend: %v\n", err) os.Exit(-2) } } diff --git a/pkg/cli/util/util.go b/pkg/cli/util/util.go new file mode 100644 index 0000000000..3a73c38afb --- /dev/null +++ b/pkg/cli/util/util.go @@ -0,0 +1,175 @@ +/* +Copyright 2019 The Vulcan 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 util + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + vcbatch "volcano.sh/volcano/pkg/apis/batch/v1alpha1" + vcbus "volcano.sh/volcano/pkg/apis/bus/v1alpha1" + "volcano.sh/volcano/pkg/apis/helpers" + "volcano.sh/volcano/pkg/client/clientset/versioned" +) + +// CommonFlags are the flags that most command lines have +type CommonFlags struct { + Master string + Kubeconfig string +} + +// InitFlags initializes the common flags for most command lines +func InitFlags(cmd *cobra.Command, cf *CommonFlags) { + cmd.Flags().StringVarP(&cf.Master, "master", "s", "", "the address of apiserver") + + kubeConfFile := os.Getenv("KUBECONFIG") + if kubeConfFile == "" { + if home := HomeDir(); home != "" { + kubeConfFile = filepath.Join(home, ".kube", "config") + } + } + cmd.Flags().StringVarP(&cf.Kubeconfig, "kubeconfig", "k", kubeConfFile, "(optional) absolute path to the kubeconfig file") +} + +// HomeDir gets the env $HOME +func HomeDir() string { + if h := os.Getenv("HOME"); h != "" { + return h + } + return os.Getenv("USERPROFILE") // windows +} + +// BuildConfig builds the configure file for command lines +func BuildConfig(master, kubeconfig string) (*rest.Config, error) { + return clientcmd.BuildConfigFromFlags(master, kubeconfig) +} + +// PopulateResourceListV1 takes strings of form =,= and returns ResourceList. +func PopulateResourceListV1(spec string) (v1.ResourceList, error) { + // empty input gets a nil response to preserve generator test expected behaviors + if spec == "" { + return nil, nil + } + + result := v1.ResourceList{} + resourceStatements := strings.Split(spec, ",") + for _, resourceStatement := range resourceStatements { + parts := strings.Split(resourceStatement, "=") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid argument syntax %v, expected =", resourceStatement) + } + resourceName := v1.ResourceName(parts[0]) + resourceQuantity, err := resource.ParseQuantity(parts[1]) + if err != nil { + return nil, err + } + result[resourceName] = resourceQuantity + } + return result, nil +} + +// CreateJobCommand executes a command such as resume/suspend +func CreateJobCommand(config *rest.Config, ns, name string, action vcbatch.Action) error { + jobClient := versioned.NewForConfigOrDie(config) + job, err := jobClient.BatchV1alpha1().Jobs(ns).Get(name, metav1.GetOptions{}) + if err != nil { + return err + } + + ctrlRef := metav1.NewControllerRef(job, helpers.JobKind) + cmd := &vcbus.Command{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("%s-%s-", + job.Name, strings.ToLower(string(action))), + Namespace: job.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *ctrlRef, + }, + }, + TargetObject: ctrlRef, + Action: string(action), + } + + if _, err := jobClient.BusV1alpha1().Commands(ns).Create(cmd); err != nil { + return err + } + + return nil +} + +// TranslateTimestampSince translates the time stamp +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/util/util_test.go b/pkg/cli/util/util_test.go new file mode 100644 index 0000000000..0e3edc7bd7 --- /dev/null +++ b/pkg/cli/util/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 util + +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/vcancel/cancel.go b/pkg/cli/vcancel/cancel.go new file mode 100644 index 0000000000..3273f305b2 --- /dev/null +++ b/pkg/cli/vcancel/cancel.go @@ -0,0 +1,66 @@ +/* +Copyright 2019 The Vulcan 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 vcancel + +import ( + "fmt" + + "github.com/spf13/cobra" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "volcano.sh/volcano/pkg/cli/util" + "volcano.sh/volcano/pkg/client/clientset/versioned" +) + +type cancelFlags struct { + util.CommonFlags + + Namespace string + JobName string +} + +var cancelJobFlags = &cancelFlags{} + +// InitCancelFlags init the cancel command flags +func InitCancelFlags(cmd *cobra.Command) { + util.InitFlags(cmd, &cancelJobFlags.CommonFlags) + + cmd.Flags().StringVarP(&cancelJobFlags.Namespace, "namespace", "N", "default", "the namespace of job") + cmd.Flags().StringVarP(&cancelJobFlags.JobName, "name", "n", "", "the name of job") +} + +// CancelJob cancel the job +func CancelJob() error { + config, err := util.BuildConfig(cancelJobFlags.Master, cancelJobFlags.Kubeconfig) + if err != nil { + return err + } + + if cancelJobFlags.JobName == "" { + err := fmt.Errorf("job name is mandatory to cancel a particular job") + return err + } + + jobClient := versioned.NewForConfigOrDie(config) + err = jobClient.BatchV1alpha1().Jobs(cancelJobFlags.Namespace).Delete(cancelJobFlags.JobName, &metav1.DeleteOptions{}) + if err != nil { + return err + } + fmt.Printf("cancel job %v successfully\n", cancelJobFlags.JobName) + return nil +} diff --git a/pkg/cli/vcancel/cancel_test.go b/pkg/cli/vcancel/cancel_test.go new file mode 100644 index 0000000000..78bd89253d --- /dev/null +++ b/pkg/cli/vcancel/cancel_test.go @@ -0,0 +1,80 @@ +/* +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 vcancel + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/spf13/cobra" + + "volcano.sh/volcano/pkg/apis/batch/v1alpha1" +) + +func TestCancelJobJob(t *testing.T) { + response := v1alpha1.Job{} + response.Name = "testJob" + + 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() + + cancelJobFlags.Master = server.URL + cancelJobFlags.Namespace = "test" + cancelJobFlags.JobName = "testJob" + + testCases := []struct { + Name string + ExpectValue error + }{ + { + Name: "CancelJob", + ExpectValue: nil, + }, + } + + for i, testcase := range testCases { + err := CancelJob() + if err != nil { + t.Errorf("case %d (%s): expected: %v, got %v ", i, testcase.Name, testcase.ExpectValue, err) + } + } + +} + +func TestInitDeleteFlags(t *testing.T) { + var cmd cobra.Command + InitCancelFlags(&cmd) + + if cmd.Flag("namespace") == nil { + t.Errorf("Could not find the flag namespace") + } + if cmd.Flag("name") == nil { + t.Errorf("Could not find the flag name") + } + +} diff --git a/pkg/cli/vjobs/view.go b/pkg/cli/vjobs/view.go new file mode 100644 index 0000000000..4085386dd2 --- /dev/null +++ b/pkg/cli/vjobs/view.go @@ -0,0 +1,385 @@ +/* +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 vjobs + +import ( + "encoding/json" + "fmt" + "io" + "os" + "strings" + + "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/cli/util" + "volcano.sh/volcano/pkg/client/clientset/versioned" +) + +type viewFlags struct { + util.CommonFlags + + Namespace string + JobName string + SchedulerName string + allNamespace bool + selector string +} + +const ( + // Level0 is the level of print indent + Level0 = iota + // Level1 is the level of print indent + Level1 + // Level2 is the level of print indent + Level2 + + // Name name etc below key words are used in job print format + Name string = "Name" + // Creation create + Creation string = "Creation" + // Phase phase + Phase string = "Phase" + // Replicas replicas + Replicas string = "Replicas" + // Min minimum + Min string = "Min" + // Scheduler scheduler + Scheduler string = "Scheduler" + // Pending pending + Pending string = "Pending" + // Running running + Running string = "Running" + // Succeeded success + Succeeded string = "Succeeded" + // Terminating terminating + Terminating string = "Terminating" + // Version version + Version string = "Version" + // Failed failed + Failed string = "Failed" + // Unknown pod + Unknown string = "Unknown" + // RetryCount retry count + RetryCount string = "RetryCount" + // JobType job type + JobType string = "JobType" + // Namespace job namespace + Namespace string = "Namespace" +) + +var viewJobFlags = &viewFlags{} + +// InitViewFlags init the view command flags +func InitViewFlags(cmd *cobra.Command) { + util.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.SchedulerName, "scheduler", "S", "", "list job with specified scheduler name") + cmd.Flags().BoolVarP(&viewJobFlags.allNamespace, "all-namespaces", "", false, "list jobs in all namespaces") + cmd.Flags().StringVarP(&viewJobFlags.selector, "selector", "", "", "fuzzy matching jobName") +} + +// ViewJob gives full details of the job +func ViewJob() error { + config, err := util.BuildConfig(viewJobFlags.Master, viewJobFlags.Kubeconfig) + if err != nil { + return err + } + if viewJobFlags.JobName == "" { + err := ListJobs() + return err + } + + jobClient := versioned.NewForConfigOrDie(config) + job, err := jobClient.BatchV1alpha1().Jobs(viewJobFlags.Namespace).Get(viewJobFlags.JobName, metav1.GetOptions{}) + if err != nil { + return err + } + if job == nil { + fmt.Printf("No resources found\n") + return nil + } + PrintJobInfo(job, os.Stdout) + PrintEvents(GetEvents(config, job), os.Stdout) + return nil +} + +// 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.Unknown > 0 { + WriteLine(writer, Level1, "Unknown: \t%d\n", job.Status.Unknown) + } + 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)", util.TranslateTimestampSince(e.LastTimestamp), e.Count, util.TranslateTimestampSince(e.FirstTimestamp)) + } else { + interval = util.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("%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...) +} + +// ListJobs lists all jobs details +func ListJobs() error { + config, err := util.BuildConfig(viewJobFlags.Master, viewJobFlags.Kubeconfig) + if err != nil { + return err + } + if viewJobFlags.allNamespace { + viewJobFlags.Namespace = "" + } + jobClient := versioned.NewForConfigOrDie(config) + jobs, err := jobClient.BatchV1alpha1().Jobs(viewJobFlags.Namespace).List(metav1.ListOptions{}) + if err != nil { + return err + } + + if len(jobs.Items) == 0 { + fmt.Printf("No resources found\n") + return nil + } + PrintJobs(jobs, os.Stdout) + + return nil +} + +// PrintJobs prints all jobs details +func PrintJobs(jobs *v1alpha1.JobList, writer io.Writer) { + maxLenInfo := getMaxLen(jobs) + + titleFormat := "%%-%ds%%-15s%%-12s%%-12s%%-12s%%-6s%%-10s%%-10s%%-12s%%-10s%%-12s%%-10s\n" + contentFormat := "%%-%ds%%-15s%%-12s%%-12s%%-12d%%-6d%%-10d%%-10d%%-12d%%-10d%%-12d%%-10d\n" + + var err error + if viewJobFlags.allNamespace { + _, err = fmt.Fprintf(writer, fmt.Sprintf("%%-%ds"+titleFormat, maxLenInfo[1], maxLenInfo[0]), + Namespace, Name, Creation, Phase, JobType, Replicas, Min, Pending, Running, Succeeded, Failed, Unknown, RetryCount) + } else { + _, err = fmt.Fprintf(writer, fmt.Sprintf(titleFormat, maxLenInfo[0]), + Name, Creation, Phase, JobType, Replicas, Min, Pending, Running, Succeeded, Failed, Unknown, RetryCount) + } + if err != nil { + fmt.Printf("Failed to print list command result: %s.\n", err) + } + + for _, job := range jobs.Items { + if viewJobFlags.SchedulerName != "" && viewJobFlags.SchedulerName != job.Spec.SchedulerName { + continue + } + if !strings.Contains(job.Name, viewJobFlags.selector) { + continue + } + replicas := int32(0) + for _, ts := range job.Spec.Tasks { + replicas += ts.Replicas + } + jobType := job.ObjectMeta.Labels[v1alpha1.JobTypeKey] + if jobType == "" { + jobType = "Batch" + } + + if viewJobFlags.allNamespace { + _, err = fmt.Fprintf(writer, fmt.Sprintf("%%-%ds"+contentFormat, maxLenInfo[1], maxLenInfo[0]), + job.Namespace, job.Name, job.CreationTimestamp.Format("2006-01-02"), job.Status.State.Phase, jobType, replicas, + job.Status.MinAvailable, job.Status.Pending, job.Status.Running, job.Status.Succeeded, job.Status.Failed, job.Status.Unknown, job.Status.RetryCount) + } else { + _, err = fmt.Fprintf(writer, fmt.Sprintf(contentFormat, maxLenInfo[0]), + job.Name, job.CreationTimestamp.Format("2006-01-02"), job.Status.State.Phase, jobType, replicas, + job.Status.MinAvailable, job.Status.Pending, job.Status.Running, job.Status.Succeeded, job.Status.Failed, job.Status.Unknown, job.Status.RetryCount) + } + if err != nil { + fmt.Printf("Failed to print list command result: %s.\n", err) + } + } +} + +func getMaxLen(jobs *v1alpha1.JobList) []int { + maxNameLen := len(Name) + maxNamespaceLen := len(Namespace) + for _, job := range jobs.Items { + if len(job.Name) > maxNameLen { + maxNameLen = len(job.Name) + } + if len(job.Namespace) > maxNamespaceLen { + maxNamespaceLen = len(job.Namespace) + } + } + + return []int{maxNameLen + 3, maxNamespaceLen + 3} +} diff --git a/pkg/cli/vqueues/get.go b/pkg/cli/vqueues/get.go new file mode 100644 index 0000000000..6262de965a --- /dev/null +++ b/pkg/cli/vqueues/get.go @@ -0,0 +1,148 @@ +/* +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 vqueues + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "volcano.sh/volcano/pkg/apis/scheduling/v1alpha2" + "volcano.sh/volcano/pkg/cli/util" + "volcano.sh/volcano/pkg/client/clientset/versioned" +) + +type getFlags struct { + util.CommonFlags + + Name string +} + +const ( + // Weight of the queue + Weight string = "Weight" + + // Name of queue + Name string = "Name" + + // Pending status of the queue + Pending string = "Pending" + + // Running status of the queue + Running string = "Running" + + // Unknown status of the queue + Unknown string = "Unknown" + + // Inqueue status of queue + Inqueue string = "Inqueue" + + // State is state of queue + State string = "State" +) + +var getQueueFlags = &getFlags{} + +// InitGetFlags is used to init all flags +func InitGetFlags(cmd *cobra.Command) { + util.InitFlags(cmd, &getQueueFlags.CommonFlags) + + cmd.Flags().StringVarP(&getQueueFlags.Name, "name", "n", "", "the name of queue") + +} + +// ListQueue lists all the queue +func ListQueue() error { + config, err := util.BuildConfig(getQueueFlags.Master, getQueueFlags.Kubeconfig) + if err != nil { + return err + } + + jobClient := versioned.NewForConfigOrDie(config) + queues, err := jobClient.SchedulingV1alpha2().Queues().List(metav1.ListOptions{}) + if err != nil { + return err + } + + if len(queues.Items) == 0 { + fmt.Printf("No resources found\n") + return nil + } + PrintQueues(queues, os.Stdout) + + return nil +} + +// PrintQueues prints queue information +func PrintQueues(queues *v1alpha2.QueueList, writer io.Writer) { + _, err := fmt.Fprintf(writer, "%-25s%-8s%-8s%-8s%-8s%-8s%-8s\n", + Name, Weight, State, Inqueue, Pending, Running, Unknown) + if err != nil { + fmt.Printf("Failed to print queue command result: %s.\n", err) + } + for _, queue := range queues.Items { + _, err = fmt.Fprintf(writer, "%-25s%-8d%-8s%-8d%-8d%-8d%-8d\n", + queue.Name, queue.Spec.Weight, queue.Status.State, queue.Status.Inqueue, + queue.Status.Pending, queue.Status.Running, queue.Status.Unknown) + if err != nil { + fmt.Printf("Failed to print queue command result: %s.\n", err) + } + } + +} + +// GetQueue gets a queue +func GetQueue() error { + config, err := util.BuildConfig(getQueueFlags.Master, getQueueFlags.Kubeconfig) + if err != nil { + return err + } + + if getQueueFlags.Name == "" { + err := ListQueue() + return err + } + + queueClient := versioned.NewForConfigOrDie(config) + queue, err := queueClient.SchedulingV1alpha2().Queues().Get(getQueueFlags.Name, metav1.GetOptions{}) + if err != nil { + return err + } + + PrintQueue(queue, os.Stdout) + + return nil +} + +// PrintQueue prints queue information +func PrintQueue(queue *v1alpha2.Queue, writer io.Writer) { + _, err := fmt.Fprintf(writer, "%-25s%-8s%-8s%-8s%-8s%-8s%-8s\n", + Name, Weight, State, Inqueue, Pending, Running, Unknown) + if err != nil { + fmt.Printf("Failed to print queue command result: %s.\n", err) + } + _, err = fmt.Fprintf(writer, "%-25s%-8d%-8s%-8d%-8d%-8d%-8d\n", + queue.Name, queue.Spec.Weight, queue.Status.State, queue.Status.Inqueue, + queue.Status.Pending, queue.Status.Running, queue.Status.Unknown) + if err != nil { + fmt.Printf("Failed to print queue command result: %s.\n", err) + } +} diff --git a/pkg/cli/vresume/resume.go b/pkg/cli/vresume/resume.go new file mode 100644 index 0000000000..730d50b56c --- /dev/null +++ b/pkg/cli/vresume/resume.go @@ -0,0 +1,59 @@ +/* +Copyright 2019 The Vulcan 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 vresume + +import ( + "fmt" + + "github.com/spf13/cobra" + + "volcano.sh/volcano/pkg/apis/batch/v1alpha1" + "volcano.sh/volcano/pkg/cli/util" +) + +type resumeFlags struct { + util.CommonFlags + + Namespace string + JobName string +} + +var resumeJobFlags = &resumeFlags{} + +// InitResumeFlags init resume command flags +func InitResumeFlags(cmd *cobra.Command) { + util.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") +} + +// ResumeJob resumes the job +func ResumeJob() error { + config, err := util.BuildConfig(resumeJobFlags.Master, resumeJobFlags.Kubeconfig) + if err != nil { + return err + } + if resumeJobFlags.JobName == "" { + err := fmt.Errorf("job name is mandatory to resume a particular job") + return err + } + + return util.CreateJobCommand(config, + resumeJobFlags.Namespace, resumeJobFlags.JobName, + v1alpha1.ResumeJobAction) +} diff --git a/pkg/cli/vresume/resume_test.go b/pkg/cli/vresume/resume_test.go new file mode 100644 index 0000000000..29400805dd --- /dev/null +++ b/pkg/cli/vresume/resume_test.go @@ -0,0 +1,91 @@ +/* +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 vresume + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/spf13/cobra" + + v1alpha1batch "volcano.sh/volcano/pkg/apis/batch/v1alpha1" + v1alpha1 "volcano.sh/volcano/pkg/apis/bus/v1alpha1" +) + +func TestResumeJob(t *testing.T) { + responsecommand := v1alpha1.Command{} + responsejob := v1alpha1batch.Job{} + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "command") { + w.Header().Set("Content-Type", "application/json") + val, err := json.Marshal(responsecommand) + if err == nil { + w.Write(val) + } + + } else { + w.Header().Set("Content-Type", "application/json") + val, err := json.Marshal(responsejob) + if err == nil { + w.Write(val) + } + + } + }) + + server := httptest.NewServer(handler) + defer server.Close() + + resumeJobFlags.Master = server.URL + resumeJobFlags.Namespace = "test" + resumeJobFlags.JobName = "testjob" + + testCases := []struct { + Name string + ExpectValue error + }{ + { + Name: "ResumeJob", + ExpectValue: nil, + }, + } + + for i, testcase := range testCases { + err := ResumeJob() + if err != nil { + t.Errorf("case %d (%s): expected: %v, got %v ", i, testcase.Name, testcase.ExpectValue, err) + } + } + +} + +func TestInitResumeFlags(t *testing.T) { + var cmd cobra.Command + InitResumeFlags(&cmd) + + if cmd.Flag("namespace") == nil { + t.Errorf("Could not find the flag namespace") + } + if cmd.Flag("name") == nil { + t.Errorf("Could not find the flag scheduler") + } + +} diff --git a/pkg/cli/vsuspend/suspend.go b/pkg/cli/vsuspend/suspend.go new file mode 100644 index 0000000000..1bed84e126 --- /dev/null +++ b/pkg/cli/vsuspend/suspend.go @@ -0,0 +1,60 @@ +/* +Copyright 2019 The Vulcan 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 vsuspend + +import ( + "fmt" + + "github.com/spf13/cobra" + + "volcano.sh/volcano/pkg/apis/batch/v1alpha1" + "volcano.sh/volcano/pkg/cli/util" +) + +type suspendFlags struct { + util.CommonFlags + + Namespace string + JobName string +} + +var suspendJobFlags = &suspendFlags{} + +// InitSuspendFlags init suspend related flags +func InitSuspendFlags(cmd *cobra.Command) { + util.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") +} + +// SuspendJob suspends the job +func SuspendJob() error { + config, err := util.BuildConfig(suspendJobFlags.Master, suspendJobFlags.Kubeconfig) + if err != nil { + return err + } + + if suspendJobFlags.JobName == "" { + err := fmt.Errorf("job name is mandatory to suspend a particular job") + return err + } + + return util.CreateJobCommand(config, + suspendJobFlags.Namespace, suspendJobFlags.JobName, + v1alpha1.AbortJobAction) +} diff --git a/pkg/cli/vsuspend/suspend_test.go b/pkg/cli/vsuspend/suspend_test.go new file mode 100644 index 0000000000..a85e7ae1dc --- /dev/null +++ b/pkg/cli/vsuspend/suspend_test.go @@ -0,0 +1,91 @@ +/* +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 vsuspend + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/spf13/cobra" + + v1alpha1batch "volcano.sh/volcano/pkg/apis/batch/v1alpha1" + v1alpha1 "volcano.sh/volcano/pkg/apis/bus/v1alpha1" +) + +func TestSuspendJobJob(t *testing.T) { + responsecommand := v1alpha1.Command{} + responsejob := v1alpha1batch.Job{} + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "command") { + w.Header().Set("Content-Type", "application/json") + val, err := json.Marshal(responsecommand) + if err == nil { + w.Write(val) + } + + } else { + w.Header().Set("Content-Type", "application/json") + val, err := json.Marshal(responsejob) + if err == nil { + w.Write(val) + } + + } + }) + + server := httptest.NewServer(handler) + defer server.Close() + + suspendJobFlags.Master = server.URL + suspendJobFlags.Namespace = "test" + suspendJobFlags.JobName = "testjob" + + testCases := []struct { + Name string + ExpectValue error + }{ + { + Name: "SuspendJob", + ExpectValue: nil, + }, + } + + for i, testcase := range testCases { + err := SuspendJob() + if err != nil { + t.Errorf("case %d (%s): expected: %v, got %v ", i, testcase.Name, testcase.ExpectValue, err) + } + } + +} + +func TestInitSuspendFlags(t *testing.T) { + var cmd cobra.Command + InitSuspendFlags(&cmd) + + if cmd.Flag("namespace") == nil { + t.Errorf("Could not find the flag namespace") + } + if cmd.Flag("name") == nil { + t.Errorf("Could not find the flag name") + } + +}