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

This commit is contained in:
Maciej Szulik 2018-02-21 22:18:59 +01:00
parent 5b57c2db00
commit 8bf4cfcf60
No known key found for this signature in database
GPG Key ID: F15E55D276FA84C4
3 changed files with 78 additions and 107 deletions

View File

@ -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(
"//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",

View File

@ -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 {
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 {
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)
} }

View File

@ -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) {
}, },
} }
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) {
} }
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)
if !strings.HasPrefix(submittedJob.ObjectMeta.Name, composedJobName) {
t.Errorf("expected '%s', got '%s'", composedJobName, submittedJob.ObjectMeta.Name)
} }
annotationsArrayLength := len(submittedJob.Annotations) err := cmdOptions.createJob(cronJob)
if annotationsArrayLength != 1 { if err != nil {
t.Errorf("expected length of annotations array to be 1, got %d", annotationsArrayLength) t.Errorf("unexpected error: %v", err)
} }
if v, ok := submittedJob.Annotations["cronjob.kubernetes.io/instantiate"]; ok { if submittedJob.ObjectMeta.Name != testJobName {
if v != "manual" { t.Errorf("expected '%s', got '%s'", testJobName, submittedJob.ObjectMeta.Name)
t.Errorf("expected annotation cronjob.kubernetes.io/instantiate to be 'manual', got '%s'", v)
}
} else {
t.Errorf("expected annotation cronjob.kubernetes.io/instantiate to exist")
} }
labelsArrayLength := len(submittedJob.Labels) if l := len(submittedJob.Annotations); l != 1 {
if labelsArrayLength != 1 { t.Errorf("expected length of annotations array to be 1, got %d", l)
t.Errorf("expected length of labels array to be 1, got %d", labelsArrayLength) }
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 v, ok := submittedJob.Labels["test-label"]; ok { if l := len(submittedJob.Labels); l != 1 {
if v != "test-value" { t.Errorf("expected length of labels array to be 1, got %d", l)
t.Errorf("expected label test-label to be 'test-value', got '%s'", v)
} }
} else { if v, ok := submittedJob.Labels["test-label"]; !ok || v != "test-value" {
t.Errorf("expected label test-label to exist") t.Errorf("expected label test-label=test-value to to exist, got '%s'", v)
} }
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)
} }