diff --git a/docs/.generated_docs b/docs/.generated_docs index 4886eea60ba..18e655dfcfc 100644 --- a/docs/.generated_docs +++ b/docs/.generated_docs @@ -172,6 +172,7 @@ docs/man/man1/kubectl-create-clusterrole.1 docs/man/man1/kubectl-create-clusterrolebinding.1 docs/man/man1/kubectl-create-configmap.1 docs/man/man1/kubectl-create-deployment.1 +docs/man/man1/kubectl-create-job.1 docs/man/man1/kubectl-create-namespace.1 docs/man/man1/kubectl-create-poddisruptionbudget.1 docs/man/man1/kubectl-create-priorityclass.1 @@ -272,6 +273,7 @@ docs/user-guide/kubectl/kubectl_create_clusterrole.md docs/user-guide/kubectl/kubectl_create_clusterrolebinding.md docs/user-guide/kubectl/kubectl_create_configmap.md docs/user-guide/kubectl/kubectl_create_deployment.md +docs/user-guide/kubectl/kubectl_create_job.md docs/user-guide/kubectl/kubectl_create_namespace.md docs/user-guide/kubectl/kubectl_create_poddisruptionbudget.md docs/user-guide/kubectl/kubectl_create_priorityclass.md diff --git a/docs/man/man1/kubectl-create-job.1 b/docs/man/man1/kubectl-create-job.1 new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/man/man1/kubectl-create-job.1 @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/docs/user-guide/kubectl/kubectl_create_job.md b/docs/user-guide/kubectl/kubectl_create_job.md new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_create_job.md @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/pkg/kubectl/cmd/BUILD b/pkg/kubectl/cmd/BUILD index 0f57c1247dd..4f76d722425 100644 --- a/pkg/kubectl/cmd/BUILD +++ b/pkg/kubectl/cmd/BUILD @@ -28,6 +28,7 @@ go_library( "create_clusterrolebinding.go", "create_configmap.go", "create_deployment.go", + "create_job.go", "create_namespace.go", "create_pdb.go", "create_priorityclass.go", @@ -142,6 +143,7 @@ go_library( "//vendor/k8s.io/apiserver/pkg/util/flag:go_default_library", "//vendor/k8s.io/client-go/discovery:go_default_library", "//vendor/k8s.io/client-go/kubernetes:go_default_library", + "//vendor/k8s.io/client-go/kubernetes/typed/batch/v1:go_default_library", "//vendor/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library", "//vendor/k8s.io/client-go/kubernetes/typed/rbac/v1:go_default_library", "//vendor/k8s.io/client-go/rest:go_default_library", @@ -171,6 +173,7 @@ go_test( "create_clusterrolebinding_test.go", "create_configmap_test.go", "create_deployment_test.go", + "create_job_test.go", "create_namespace_test.go", "create_pdb_test.go", "create_priorityclass_test.go", @@ -231,6 +234,8 @@ go_test( "//vendor/github.com/spf13/cobra:go_default_library", "//vendor/github.com/stretchr/testify/assert:go_default_library", "//vendor/gopkg.in/yaml.v2:go_default_library", + "//vendor/k8s.io/api/batch/v1:go_default_library", + "//vendor/k8s.io/api/batch/v1beta1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/api/policy/v1beta1:go_default_library", "//vendor/k8s.io/api/rbac/v1:go_default_library", @@ -252,6 +257,7 @@ go_test( "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//vendor/k8s.io/apimachinery/pkg/version:go_default_library", "//vendor/k8s.io/client-go/dynamic:go_default_library", + "//vendor/k8s.io/client-go/kubernetes/fake:go_default_library", "//vendor/k8s.io/client-go/rest:go_default_library", "//vendor/k8s.io/client-go/rest/fake:go_default_library", "//vendor/k8s.io/client-go/testing:go_default_library", diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 8c02c098e3a..6784fef85e0 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -115,6 +115,7 @@ func NewCmdCreate(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { cmd.AddCommand(NewCmdCreateRoleBinding(f, out)) cmd.AddCommand(NewCmdCreatePodDisruptionBudget(f, out)) cmd.AddCommand(NewCmdCreatePriorityClass(f, out)) + cmd.AddCommand(NewCmdCreateJob(f, out)) return cmd } diff --git a/pkg/kubectl/cmd/create_job.go b/pkg/kubectl/cmd/create_job.go new file mode 100644 index 00000000000..382ca5c346e --- /dev/null +++ b/pkg/kubectl/cmd/create_job.go @@ -0,0 +1,146 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + batchv1 "k8s.io/api/batch/v1" + batchv1beta1 "k8s.io/api/batch/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientbatchv1 "k8s.io/client-go/kubernetes/typed/batch/v1" + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/kubectl/util/i18n" +) + +var ( + jobLong = templates.LongDesc(i18n.T(` + Create a job with the specified name.`)) + + jobExample = templates.Examples(i18n.T(` + # Create a job from a CronJob named "a-cronjob" + kubectl create job --from=cronjob/a-cronjob`)) +) + +type CreateJobOptions struct { + Name string + From string + + Namespace string + Client clientbatchv1.BatchV1Interface + Out io.Writer + DryRun bool + Builder *resource.Builder + Cmd *cobra.Command +} + +// NewCmdCreateJob is a command to ease creating Jobs from CronJobs. +func NewCmdCreateJob(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command { + c := &CreateJobOptions{ + Out: cmdOut, + } + cmd := &cobra.Command{ + Use: "job NAME [--from-cronjob=CRONJOB]", + Short: jobLong, + Long: jobLong, + Example: jobExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(c.Complete(f, cmd, args)) + cmdutil.CheckErr(c.RunCreateJob()) + }, + } + cmdutil.AddApplyAnnotationFlags(cmd) + cmdutil.AddValidateFlags(cmd) + cmdutil.AddPrinterFlags(cmd) + cmd.Flags().String("from", "", "The name of the resource to create a Job from (only cronjob is supported).") + + return cmd +} + +func (c *CreateJobOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) (err error) { + if len(args) == 0 { + return cmdutil.UsageErrorf(cmd, "NAME is required") + } + c.Name = args[0] + + c.From = cmdutil.GetFlagString(cmd, "from") + c.Namespace, _, err = f.DefaultNamespace() + if err != nil { + return err + } + + clientset, err := f.KubernetesClientSet() + if err != nil { + return err + } + c.Client = clientset.BatchV1() + c.Builder = f.NewBuilder() + c.Cmd = cmd + + return nil +} + +func (c *CreateJobOptions) RunCreateJob() error { + infos, err := c.Builder. + Unstructured(). + NamespaceParam(c.Namespace).DefaultNamespace(). + ResourceTypeOrNameArgs(false, c.From). + Flatten(). + Latest(). + Do(). + Infos() + if err != nil { + return err + } + if len(infos) != 1 { + return fmt.Errorf("from must be an existing cronjob") + } + cronJob, ok := infos[0].AsVersioned().(*batchv1beta1.CronJob) + if !ok { + return fmt.Errorf("from must be an existing cronjob") + } + + return c.createJob(cronJob) +} + +func (c *CreateJobOptions) createJob(cronJob *batchv1beta1.CronJob) error { + annotations := make(map[string]string) + annotations["cronjob.kubernetes.io/instantiate"] = "manual" + for k, v := range cronJob.Spec.JobTemplate.Annotations { + annotations[k] = v + } + jobToCreate := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: c.Name, + Namespace: c.Namespace, + Annotations: annotations, + Labels: cronJob.Spec.JobTemplate.Labels, + }, + Spec: cronJob.Spec.JobTemplate.Spec, + } + + job, err := c.Client.Jobs(c.Namespace).Create(jobToCreate) + if err != nil { + return fmt.Errorf("failed to create job: %v", err) + } + return cmdutil.PrintObject(c.Cmd, job, c.Out) +} diff --git a/pkg/kubectl/cmd/create_job_test.go b/pkg/kubectl/cmd/create_job_test.go new file mode 100644 index 00000000000..4fb54df19c2 --- /dev/null +++ b/pkg/kubectl/cmd/create_job_test.go @@ -0,0 +1,123 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "bytes" + "testing" + + batchv1 "k8s.io/api/batch/v1" + batchv1beta1 "k8s.io/api/batch/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + fake "k8s.io/client-go/kubernetes/fake" + clienttesting "k8s.io/client-go/testing" + cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" +) + +func TestCreateJobFromCronJob(t *testing.T) { + var submittedJob *batchv1.Job + testNamespaceName := "test" + testCronJobName := "test-cronjob" + testJobName := "test-job" + testImageName := "fake" + + expectedLabels := make(map[string]string) + expectedAnnotations := make(map[string]string) + expectedLabels["test-label"] = "test-value" + + expectJob := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespaceName, + Labels: expectedLabels, + Annotations: expectedAnnotations, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Image: testImageName}, + }, + }, + }, + }, + } + + cronJob := &batchv1beta1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Name: testCronJobName, + Namespace: testNamespaceName, + }, + Spec: batchv1beta1.CronJobSpec{ + Schedule: "* * * * *", + JobTemplate: batchv1beta1.JobTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespaceName, + Labels: expectedLabels, + }, + Spec: expectJob.Spec, + }, + }, + } + + clientset := fake.Clientset{} + clientset.PrependReactor("create", "jobs", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + ca := action.(clienttesting.CreateAction) + submittedJob = ca.GetObject().(*batchv1.Job) + return true, expectJob, nil + }) + f, _ := cmdtesting.NewAPIFactory() + buf := bytes.NewBuffer([]byte{}) + cmdOptions := &CreateJobOptions{ + Name: testJobName, + Namespace: testNamespaceName, + Client: clientset.BatchV1(), + Out: buf, + Cmd: NewCmdCreateJob(f, buf), + } + + err := cmdOptions.createJob(cronJob) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if submittedJob.ObjectMeta.Name != testJobName { + t.Errorf("expected '%s', got '%s'", testJobName, submittedJob.ObjectMeta.Name) + } + + if l := len(submittedJob.Annotations); l != 1 { + t.Errorf("expected length of annotations array to be 1, got %d", l) + } + if v, ok := submittedJob.Annotations["cronjob.kubernetes.io/instantiate"]; !ok || v != "manual" { + t.Errorf("expected annotation cronjob.kubernetes.io/instantiate=manual to exist, got '%s'", v) + } + + if l := len(submittedJob.Labels); l != 1 { + t.Errorf("expected length of labels array to be 1, got %d", l) + } + if v, ok := submittedJob.Labels["test-label"]; !ok || v != "test-value" { + t.Errorf("expected label test-label=test-value to to exist, got '%s'", v) + } + + if l := len(submittedJob.Spec.Template.Spec.Containers); l != 1 { + t.Errorf("expected length of container array to be 1, got %d", l) + } + if submittedJob.Spec.Template.Spec.Containers[0].Image != testImageName { + t.Errorf("expected '%s', got '%s'", testImageName, submittedJob.Spec.Template.Spec.Containers[0].Image) + } +}