Add kubectl create job --from=cronjob/<name>

parent 5b57c2db
...@@ -133,7 +133,6 @@ go_library( ...@@ -133,7 +133,6 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/util/jsonmergepatch:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/jsonmergepatch:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/mergepatch:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/mergepatch:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/rand:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/validation:go_default_library",
...@@ -145,7 +144,6 @@ go_library( ...@@ -145,7 +144,6 @@ go_library(
"//vendor/k8s.io/client-go/discovery: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:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/typed/batch/v1:go_default_library", "//vendor/k8s.io/client-go/kubernetes/typed/batch/v1:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/typed/batch/v1beta1:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/typed/core/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/kubernetes/typed/rbac/v1:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library", "//vendor/k8s.io/client-go/rest:go_default_library",
......
...@@ -23,37 +23,34 @@ import ( ...@@ -23,37 +23,34 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
batchv1 "k8s.io/api/batch/v1" batchv1 "k8s.io/api/batch/v1"
"k8s.io/apimachinery/pkg/api/meta" batchv1beta1 "k8s.io/api/batch/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/rand"
clientbatchv1 "k8s.io/client-go/kubernetes/typed/batch/v1" clientbatchv1 "k8s.io/client-go/kubernetes/typed/batch/v1"
clientbatchv1beta1 "k8s.io/client-go/kubernetes/typed/batch/v1beta1"
"k8s.io/kubernetes/pkg/kubectl/cmd/templates" "k8s.io/kubernetes/pkg/kubectl/cmd/templates"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/kubectl/util/i18n" "k8s.io/kubernetes/pkg/kubectl/util/i18n"
) )
var ( var (
jobLong = templates.LongDesc(i18n.T(` jobLong = templates.LongDesc(i18n.T(`
Create a Job to execute immediately from a specified CronJob.`)) Create a job with the specified name.`))
jobExample = templates.Examples(i18n.T(` jobExample = templates.Examples(i18n.T(`
# Create a Job from a CronJob named "a-cronjob" # Create a job from a CronJob named "a-cronjob"
kubectl create job --from-cronjob=a-cronjob`)) kubectl create job --from=cronjob/a-cronjob`))
) )
type CreateJobOptions struct { type CreateJobOptions struct {
FromCronJob string Name string
From string
OutputFormat string
Namespace string Namespace string
V1Beta1Client clientbatchv1beta1.BatchV1beta1Interface Client clientbatchv1.BatchV1Interface
V1Client clientbatchv1.BatchV1Interface
Mapper meta.RESTMapper
Out io.Writer Out io.Writer
PrintObject func(obj runtime.Object) error DryRun bool
PrintSuccess func(mapper meta.RESTMapper, shortOutput bool, out io.Writer, resource, name string, dryRun bool, operation string) Builder *resource.Builder
Cmd *cobra.Command
} }
// NewCmdCreateJob is a command to ease creating Jobs from CronJobs. // NewCmdCreateJob is a command to ease creating Jobs from CronJobs.
...@@ -62,7 +59,7 @@ func NewCmdCreateJob(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command { ...@@ -62,7 +59,7 @@ func NewCmdCreateJob(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command {
Out: cmdOut, Out: cmdOut,
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "job --from-cronjob=CRONJOB", Use: "job NAME [--from-cronjob=CRONJOB]",
Short: jobLong, Short: jobLong,
Long: jobLong, Long: jobLong,
Example: jobExample, Example: jobExample,
...@@ -74,74 +71,76 @@ func NewCmdCreateJob(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command { ...@@ -74,74 +71,76 @@ func NewCmdCreateJob(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command {
cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd)
cmdutil.AddValidateFlags(cmd) cmdutil.AddValidateFlags(cmd)
cmdutil.AddPrinterFlags(cmd) cmdutil.AddPrinterFlags(cmd)
cmd.Flags().String("from-cronjob", "", "Specify the name of the CronJob to create a Job from.") cmd.Flags().String("from", "", "The name of the resource to create a Job from (only cronjob is supported).")
return cmd return cmd
} }
func (c *CreateJobOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) (err error) { func (c *CreateJobOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) (err error) {
c.FromCronJob = cmdutil.GetFlagString(cmd, "from-cronjob") if len(args) == 0 {
return cmdutil.UsageErrorf(cmd, "NAME is required")
// Complete other options for Run. }
c.Mapper, _ = f.Object() c.Name = args[0]
c.OutputFormat = cmdutil.GetFlagString(cmd, "output")
c.From = cmdutil.GetFlagString(cmd, "from")
c.Namespace, _, err = f.DefaultNamespace() c.Namespace, _, err = f.DefaultNamespace()
if err != nil { if err != nil {
return err return err
} }
c.PrintObject = func(obj runtime.Object) error {
return f.PrintObject(cmd, obj, c.Out)
}
// need two client sets to deal with the differing versions of CronJobs and Jobs
clientset, err := f.KubernetesClientSet() clientset, err := f.KubernetesClientSet()
if err != nil { if err != nil {
return err return err
} }
if c.V1Client == nil { c.Client = clientset.BatchV1()
c.V1Client = clientset.BatchV1() c.Builder = f.NewBuilder()
} c.Cmd = cmd
if c.V1Beta1Client == nil {
c.V1Beta1Client = clientset.BatchV1beta1()
}
return nil return nil
} }
func (c *CreateJobOptions) RunCreateJob() (err error) { func (c *CreateJobOptions) RunCreateJob() error {
cronjob, err := c.V1Beta1Client.CronJobs(c.Namespace).Get(c.FromCronJob, metav1.GetOptions{}) infos, err := c.Builder.
Unstructured().
NamespaceParam(c.Namespace).DefaultNamespace().
ResourceTypeOrNameArgs(false, c.From).
Flatten().
Latest().
Do().
Infos()
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch job: %v", err) 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 := make(map[string]string)
annotations["cronjob.kubernetes.io/instantiate"] = "manual" annotations["cronjob.kubernetes.io/instantiate"] = "manual"
for k, v := range cronJob.Spec.JobTemplate.Annotations {
labels := make(map[string]string) annotations[k] = v
for k, v := range cronjob.Spec.JobTemplate.Labels {
labels[k] = v
} }
jobToCreate := &batchv1.Job{ jobToCreate := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
// job name cannot exceed DNS1053LabelMaxLength (52 characters) Name: c.Name,
Name: cronjob.Name + "-manual-" + rand.String(3),
Namespace: c.Namespace, Namespace: c.Namespace,
Annotations: annotations, Annotations: annotations,
Labels: labels, Labels: cronJob.Spec.JobTemplate.Labels,
}, },
Spec: cronjob.Spec.JobTemplate.Spec, Spec: cronJob.Spec.JobTemplate.Spec,
} }
result, err := c.V1Client.Jobs(c.Namespace).Create(jobToCreate) job, err := c.Client.Jobs(c.Namespace).Create(jobToCreate)
if err != nil { if err != nil {
return fmt.Errorf("failed to create job: %v", err) return fmt.Errorf("failed to create job: %v", err)
} }
return cmdutil.PrintObject(c.Cmd, job, c.Out)
return c.PrintObject(result)
} }
...@@ -18,32 +18,28 @@ package cmd ...@@ -18,32 +18,28 @@ package cmd
import ( import (
"bytes" "bytes"
"fmt"
"strings"
"testing" "testing"
"k8s.io/apimachinery/pkg/runtime"
batchv1 "k8s.io/api/batch/v1" batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1" batchv1beta1 "k8s.io/api/batch/v1beta1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
fake "k8s.io/client-go/kubernetes/fake" fake "k8s.io/client-go/kubernetes/fake"
clienttesting "k8s.io/client-go/testing" clienttesting "k8s.io/client-go/testing"
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
) )
var submittedJob *batchv1.Job
func TestCreateJobFromCronJob(t *testing.T) { func TestCreateJobFromCronJob(t *testing.T) {
var submittedJob *batchv1.Job
testNamespaceName := "test" testNamespaceName := "test"
testCronJobName := "test-cronjob" testCronJobName := "test-cronjob"
testJobName := "test-job"
testImageName := "fake" testImageName := "fake"
expectedLabels := make(map[string]string) expectedLabels := make(map[string]string)
expectedAnnotations := make(map[string]string) expectedAnnotations := make(map[string]string)
expectedLabels["test-label"] = "test-value" expectedLabels["test-label"] = "test-value"
expectedAnnotations["cronjob.kubernetes.io/instantiate"] = "manual"
expectJob := &batchv1.Job{ expectJob := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
...@@ -62,7 +58,7 @@ func TestCreateJobFromCronJob(t *testing.T) { ...@@ -62,7 +58,7 @@ func TestCreateJobFromCronJob(t *testing.T) {
}, },
} }
cronJobToCreate := &batchv1beta1.CronJob{ cronJob := &batchv1beta1.CronJob{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: testCronJobName, Name: testCronJobName,
Namespace: testNamespaceName, Namespace: testNamespaceName,
...@@ -80,69 +76,47 @@ func TestCreateJobFromCronJob(t *testing.T) { ...@@ -80,69 +76,47 @@ func TestCreateJobFromCronJob(t *testing.T) {
} }
clientset := fake.Clientset{} clientset := fake.Clientset{}
clientset.PrependReactor("get", "cronjobs", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, cronJobToCreate, nil
})
clientset.PrependReactor("create", "jobs", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { clientset.PrependReactor("create", "jobs", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
ca := action.(clienttesting.CreateAction) ca := action.(clienttesting.CreateAction)
submittedJob = ca.GetObject().(*batchv1.Job) submittedJob = ca.GetObject().(*batchv1.Job)
return true, expectJob, nil return true, expectJob, nil
}) })
f, _ := cmdtesting.NewAPIFactory()
f, _, _, _ := cmdtesting.NewAPIFactory()
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
mapper, _ := f.Object()
cmdOptions := &CreateJobOptions{ cmdOptions := &CreateJobOptions{
Out: buf, Name: testJobName,
OutputFormat: "na",
Namespace: testNamespaceName, Namespace: testNamespaceName,
FromCronJob: testCronJobName, Client: clientset.BatchV1(),
V1Client: clientset.BatchV1(), Out: buf,
V1Beta1Client: clientset.BatchV1beta1(), Cmd: NewCmdCreateJob(f, buf),
Mapper: mapper,
PrintObject: func(obj runtime.Object) error {
return nil
},
} }
cmdOptions.RunCreateJob()
composedJobName := fmt.Sprintf("%s-manual-", testCronJobName) err := cmdOptions.createJob(cronJob)
if !strings.HasPrefix(submittedJob.ObjectMeta.Name, composedJobName) { if err != nil {
t.Errorf("expected '%s', got '%s'", composedJobName, submittedJob.ObjectMeta.Name) t.Errorf("unexpected error: %v", err)
} }
annotationsArrayLength := len(submittedJob.Annotations) if submittedJob.ObjectMeta.Name != testJobName {
if annotationsArrayLength != 1 { t.Errorf("expected '%s', got '%s'", testJobName, submittedJob.ObjectMeta.Name)
t.Errorf("expected length of annotations array to be 1, got %d", annotationsArrayLength)
} }
if v, ok := submittedJob.Annotations["cronjob.kubernetes.io/instantiate"]; ok { if l := len(submittedJob.Annotations); l != 1 {
if v != "manual" { t.Errorf("expected length of annotations array to be 1, got %d", l)
t.Errorf("expected annotation cronjob.kubernetes.io/instantiate to be 'manual', got '%s'", v)
} }
} else { if v, ok := submittedJob.Annotations["cronjob.kubernetes.io/instantiate"]; !ok || v != "manual" {
t.Errorf("expected annotation cronjob.kubernetes.io/instantiate to exist") t.Errorf("expected annotation cronjob.kubernetes.io/instantiate=manual to exist, got '%s'", v)
} }
labelsArrayLength := len(submittedJob.Labels) if l := len(submittedJob.Labels); l != 1 {
if labelsArrayLength != 1 { t.Errorf("expected length of labels array to be 1, got %d", l)
t.Errorf("expected length of labels array to be 1, got %d", labelsArrayLength)
} }
if v, ok := submittedJob.Labels["test-label"]; !ok || v != "test-value" {
if v, ok := submittedJob.Labels["test-label"]; ok { t.Errorf("expected label test-label=test-value to to exist, got '%s'", v)
if v != "test-value" {
t.Errorf("expected label test-label to be 'test-value', got '%s'", v)
}
} else {
t.Errorf("expected label test-label to exist")
} }
containerArrayLength := len(submittedJob.Spec.Template.Spec.Containers) if l := len(submittedJob.Spec.Template.Spec.Containers); l != 1 {
if containerArrayLength != 1 { t.Errorf("expected length of container array to be 1, got %d", l)
t.Errorf("expected length of container array to be 1, got %d", containerArrayLength)
} }
if submittedJob.Spec.Template.Spec.Containers[0].Image != testImageName { if submittedJob.Spec.Template.Spec.Containers[0].Image != testImageName {
t.Errorf("expected '%s', got '%s'", testImageName, submittedJob.Spec.Template.Spec.Containers[0].Image) t.Errorf("expected '%s', got '%s'", testImageName, submittedJob.Spec.Template.Spec.Containers[0].Image)
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment