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 Client clientbatchv1.BatchV1Interface
V1Beta1Client clientbatchv1beta1.BatchV1beta1Interface Out io.Writer
V1Client clientbatchv1.BatchV1Interface DryRun bool
Mapper meta.RESTMapper Builder *resource.Builder
Out io.Writer Cmd *cobra.Command
PrintObject func(obj runtime.Object) error
PrintSuccess func(mapper meta.RESTMapper, shortOutput bool, out io.Writer, resource, name string, dryRun bool, operation string)
} }
// 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, Client: clientset.BatchV1(),
FromCronJob: testCronJobName, Out: buf,
V1Client: clientset.BatchV1(), Cmd: NewCmdCreateJob(f, buf),
V1Beta1Client: clientset.BatchV1beta1(),
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) }
} if v, ok := submittedJob.Labels["test-label"]; !ok || v != "test-value" {
} else { t.Errorf("expected label test-label=test-value to to exist, got '%s'", v)
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)
} }