diff --git a/pkg/cmd/create/create_cronjob.go b/pkg/cmd/create/create_cronjob.go index a7953703..f0b672c4 100644 --- a/pkg/cmd/create/create_cronjob.go +++ b/pkg/cmd/create/create_cronjob.go @@ -29,6 +29,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" + batchv1client "k8s.io/client-go/kubernetes/typed/batch/v1" batchv1beta1client "k8s.io/client-go/kubernetes/typed/batch/v1beta1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" @@ -63,7 +64,8 @@ type CreateCronJobOptions struct { Namespace string EnforceNamespace bool - Client batchv1beta1client.BatchV1beta1Interface + ClientBeta batchv1beta1client.BatchV1beta1Interface + Client batchv1client.BatchV1Interface DryRunStrategy cmdutil.DryRunStrategy DryRunVerifier *resource.DryRunVerifier Builder *resource.Builder @@ -130,10 +132,34 @@ func (o *CreateCronJobOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, a if err != nil { return err } - o.Client, err = batchv1beta1client.NewForConfig(clientConfig) + + // TODO: drop this condition when beta disappears in 1.25 + clientset, err := f.KubernetesClientSet() if err != nil { return err } + resources, err := clientset.Discovery().ServerResourcesForGroupVersion(batchv1.SchemeGroupVersion.String()) + if err != nil { + return fmt.Errorf("failed to discover supported resources: %v", err) + } + found := false + for _, serverResource := range resources.APIResources { + if serverResource.Name == "cronjobs" { + found = true + break + } + } + if found { + o.Client, err = batchv1client.NewForConfig(clientConfig) + if err != nil { + return err + } + } else { + o.ClientBeta, err = batchv1beta1client.NewForConfig(clientConfig) + if err != nil { + return err + } + } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { @@ -166,9 +192,36 @@ func (o *CreateCronJobOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, a // Run performs the execution of 'create cronjob' sub command func (o *CreateCronJobOptions) Run() error { - cronjob := o.createCronJob() + if o.Client != nil { + cronJob := o.createCronJob() + if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, cronJob, scheme.DefaultJSONEncoder()); err != nil { + return err + } - if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, cronjob, scheme.DefaultJSONEncoder()); err != nil { + if o.DryRunStrategy != cmdutil.DryRunClient { + createOptions := metav1.CreateOptions{} + if o.FieldManager != "" { + createOptions.FieldManager = o.FieldManager + } + if o.DryRunStrategy == cmdutil.DryRunServer { + if err := o.DryRunVerifier.HasSupport(cronJob.GroupVersionKind()); err != nil { + return err + } + createOptions.DryRun = []string{metav1.DryRunAll} + } + var err error + cronJob, err = o.Client.CronJobs(o.Namespace).Create(context.TODO(), cronJob, createOptions) + if err != nil { + return fmt.Errorf("failed to create cronjob: %v", err) + } + } + + return o.PrintObj(cronJob) + } + + // TODO: drop this condition when beta disappears in 1.25 + cronJobBeta := o.createCronJobBeta() + if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, cronJobBeta, scheme.DefaultJSONEncoder()); err != nil { return err } @@ -178,22 +231,23 @@ func (o *CreateCronJobOptions) Run() error { createOptions.FieldManager = o.FieldManager } if o.DryRunStrategy == cmdutil.DryRunServer { - if err := o.DryRunVerifier.HasSupport(cronjob.GroupVersionKind()); err != nil { + if err := o.DryRunVerifier.HasSupport(cronJobBeta.GroupVersionKind()); err != nil { return err } createOptions.DryRun = []string{metav1.DryRunAll} } var err error - cronjob, err = o.Client.CronJobs(o.Namespace).Create(context.TODO(), cronjob, createOptions) + cronJobBeta, err = o.ClientBeta.CronJobs(o.Namespace).Create(context.TODO(), cronJobBeta, createOptions) if err != nil { return fmt.Errorf("failed to create cronjob: %v", err) } } - return o.PrintObj(cronjob) + return o.PrintObj(cronJobBeta) + } -func (o *CreateCronJobOptions) createCronJob() *batchv1beta1.CronJob { +func (o *CreateCronJobOptions) createCronJobBeta() *batchv1beta1.CronJob { cronjob := &batchv1beta1.CronJob{ TypeMeta: metav1.TypeMeta{APIVersion: batchv1beta1.SchemeGroupVersion.String(), Kind: "CronJob"}, ObjectMeta: metav1.ObjectMeta{ @@ -227,3 +281,38 @@ func (o *CreateCronJobOptions) createCronJob() *batchv1beta1.CronJob { } return cronjob } + +func (o *CreateCronJobOptions) createCronJob() *batchv1.CronJob { + cronjob := &batchv1.CronJob{ + TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "CronJob"}, + ObjectMeta: metav1.ObjectMeta{ + Name: o.Name, + }, + Spec: batchv1.CronJobSpec{ + Schedule: o.Schedule, + JobTemplate: batchv1.JobTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: o.Name, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: o.Name, + Image: o.Image, + Command: o.Command, + }, + }, + RestartPolicy: corev1.RestartPolicy(o.Restart), + }, + }, + }, + }, + }, + } + if o.EnforceNamespace { + cronjob.Namespace = o.Namespace + } + return cronjob +} diff --git a/pkg/cmd/create/create_cronjob_test.go b/pkg/cmd/create/create_cronjob_test.go index cf0f911d..ae5cf724 100644 --- a/pkg/cmd/create/create_cronjob_test.go +++ b/pkg/cmd/create/create_cronjob_test.go @@ -20,7 +20,6 @@ import ( "testing" batchv1 "k8s.io/api/batch/v1" - batchv1beta1 "k8s.io/api/batch/v1beta1" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -33,20 +32,20 @@ func TestCreateCronJob(t *testing.T) { command []string schedule string restart string - expected *batchv1beta1.CronJob + expected *batchv1.CronJob }{ "just image and OnFailure restart policy": { image: "busybox", schedule: "0/5 * * * ?", restart: "OnFailure", - expected: &batchv1beta1.CronJob{ - TypeMeta: metav1.TypeMeta{APIVersion: batchv1beta1.SchemeGroupVersion.String(), Kind: "CronJob"}, + expected: &batchv1.CronJob{ + TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "CronJob"}, ObjectMeta: metav1.ObjectMeta{ Name: cronjobName, }, - Spec: batchv1beta1.CronJobSpec{ + Spec: batchv1.CronJobSpec{ Schedule: "0/5 * * * ?", - JobTemplate: batchv1beta1.JobTemplateSpec{ + JobTemplate: batchv1.JobTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: cronjobName, }, @@ -72,14 +71,14 @@ func TestCreateCronJob(t *testing.T) { command: []string{"date"}, schedule: "0/5 * * * ?", restart: "Never", - expected: &batchv1beta1.CronJob{ - TypeMeta: metav1.TypeMeta{APIVersion: batchv1beta1.SchemeGroupVersion.String(), Kind: "CronJob"}, + expected: &batchv1.CronJob{ + TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "CronJob"}, ObjectMeta: metav1.ObjectMeta{ Name: cronjobName, }, - Spec: batchv1beta1.CronJobSpec{ + Spec: batchv1.CronJobSpec{ Schedule: "0/5 * * * ?", - JobTemplate: batchv1beta1.JobTemplateSpec{ + JobTemplate: batchv1.JobTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: cronjobName, }, diff --git a/pkg/describe/describe.go b/pkg/describe/describe.go index 4d86c826..2580c783 100644 --- a/pkg/describe/describe.go +++ b/pkg/describe/describe.go @@ -215,6 +215,7 @@ func describerMap(clientConfig *rest.Config) (map[schema.GroupKind]ResourceDescr {Group: networkingv1.GroupName, Kind: "IngressClass"}: &IngressClassDescriber{c}, {Group: batchv1.GroupName, Kind: "Job"}: &JobDescriber{c}, {Group: batchv1.GroupName, Kind: "CronJob"}: &CronJobDescriber{c}, + {Group: batchv1beta1.GroupName, Kind: "CronJob"}: &CronJobDescriber{c}, {Group: appsv1.GroupName, Kind: "StatefulSet"}: &StatefulSetDescriber{c}, {Group: appsv1.GroupName, Kind: "Deployment"}: &DeploymentDescriber{c}, {Group: appsv1.GroupName, Kind: "DaemonSet"}: &DaemonSetDescriber{c}, @@ -2211,19 +2212,28 @@ type CronJobDescriber struct { } func (d *CronJobDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { - cronJob, err := d.client.BatchV1beta1().CronJobs(namespace).Get(context.TODO(), name, metav1.GetOptions{}) + var events *corev1.EventList + + cronJob, err := d.client.BatchV1().CronJobs(namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err == nil { + if describerSettings.ShowEvents { + events, _ = d.client.CoreV1().Events(namespace).Search(scheme.Scheme, cronJob) + } + return describeCronJob(cronJob, events) + } + + // TODO: drop this condition when beta disappears in 1.25 + cronJobBeta, err := d.client.BatchV1beta1().CronJobs(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } - - var events *corev1.EventList if describerSettings.ShowEvents { events, _ = d.client.CoreV1().Events(namespace).Search(scheme.Scheme, cronJob) } - return describeCronJob(cronJob, events) + return describeCronJobBeta(cronJobBeta, events) } -func describeCronJob(cronJob *batchv1beta1.CronJob, events *corev1.EventList) (string, error) { +func describeCronJob(cronJob *batchv1.CronJob, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", cronJob.Name) @@ -2262,7 +2272,72 @@ func describeCronJob(cronJob *batchv1beta1.CronJob, events *corev1.EventList) (s }) } -func describeJobTemplate(jobTemplate batchv1beta1.JobTemplateSpec, w PrefixWriter) { +func describeJobTemplate(jobTemplate batchv1.JobTemplateSpec, w PrefixWriter) { + if jobTemplate.Spec.Selector != nil { + if selector, err := metav1.LabelSelectorAsSelector(jobTemplate.Spec.Selector); err == nil { + w.Write(LEVEL_0, "Selector:\t%s\n", selector) + } else { + w.Write(LEVEL_0, "Selector:\tFailed to get selector: %s\n", err) + } + } else { + w.Write(LEVEL_0, "Selector:\t\n") + } + if jobTemplate.Spec.Parallelism != nil { + w.Write(LEVEL_0, "Parallelism:\t%d\n", *jobTemplate.Spec.Parallelism) + } else { + w.Write(LEVEL_0, "Parallelism:\t\n") + } + if jobTemplate.Spec.Completions != nil { + w.Write(LEVEL_0, "Completions:\t%d\n", *jobTemplate.Spec.Completions) + } else { + w.Write(LEVEL_0, "Completions:\t\n") + } + if jobTemplate.Spec.ActiveDeadlineSeconds != nil { + w.Write(LEVEL_0, "Active Deadline Seconds:\t%ds\n", *jobTemplate.Spec.ActiveDeadlineSeconds) + } + DescribePodTemplate(&jobTemplate.Spec.Template, w) +} + +func describeCronJobBeta(cronJob *batchv1beta1.CronJob, events *corev1.EventList) (string, error) { + return tabbedString(func(out io.Writer) error { + w := NewPrefixWriter(out) + w.Write(LEVEL_0, "Name:\t%s\n", cronJob.Name) + w.Write(LEVEL_0, "Namespace:\t%s\n", cronJob.Namespace) + printLabelsMultiline(w, "Labels", cronJob.Labels) + printAnnotationsMultiline(w, "Annotations", cronJob.Annotations) + w.Write(LEVEL_0, "Schedule:\t%s\n", cronJob.Spec.Schedule) + w.Write(LEVEL_0, "Concurrency Policy:\t%s\n", cronJob.Spec.ConcurrencyPolicy) + w.Write(LEVEL_0, "Suspend:\t%s\n", printBoolPtr(cronJob.Spec.Suspend)) + if cronJob.Spec.SuccessfulJobsHistoryLimit != nil { + w.Write(LEVEL_0, "Successful Job History Limit:\t%d\n", *cronJob.Spec.SuccessfulJobsHistoryLimit) + } else { + w.Write(LEVEL_0, "Successful Job History Limit:\t\n") + } + if cronJob.Spec.FailedJobsHistoryLimit != nil { + w.Write(LEVEL_0, "Failed Job History Limit:\t%d\n", *cronJob.Spec.FailedJobsHistoryLimit) + } else { + w.Write(LEVEL_0, "Failed Job History Limit:\t\n") + } + if cronJob.Spec.StartingDeadlineSeconds != nil { + w.Write(LEVEL_0, "Starting Deadline Seconds:\t%ds\n", *cronJob.Spec.StartingDeadlineSeconds) + } else { + w.Write(LEVEL_0, "Starting Deadline Seconds:\t\n") + } + describeJobTemplateBeta(cronJob.Spec.JobTemplate, w) + if cronJob.Status.LastScheduleTime != nil { + w.Write(LEVEL_0, "Last Schedule Time:\t%s\n", cronJob.Status.LastScheduleTime.Time.Format(time.RFC1123Z)) + } else { + w.Write(LEVEL_0, "Last Schedule Time:\t\n") + } + printActiveJobs(w, "Active Jobs", cronJob.Status.Active) + if events != nil { + DescribeEvents(events, w) + } + return nil + }) +} + +func describeJobTemplateBeta(jobTemplate batchv1beta1.JobTemplateSpec, w PrefixWriter) { if jobTemplate.Spec.Selector != nil { if selector, err := metav1.LabelSelectorAsSelector(jobTemplate.Spec.Selector); err == nil { w.Write(LEVEL_0, "Selector:\t%s\n", selector) diff --git a/pkg/polymorphichelpers/updatepodspec.go b/pkg/polymorphichelpers/updatepodspec.go index c1368552..f386447c 100644 --- a/pkg/polymorphichelpers/updatepodspec.go +++ b/pkg/polymorphichelpers/updatepodspec.go @@ -81,6 +81,8 @@ func updatePodSpecForObject(obj runtime.Object, fn func(*v1.PodSpec) error) (boo // CronJob case *batchv1beta1.CronJob: return true, fn(&t.Spec.JobTemplate.Spec.Template.Spec) + case *batchv1.CronJob: + return true, fn(&t.Spec.JobTemplate.Spec.Template.Spec) default: return false, fmt.Errorf("the object is not a pod or does not have a pod template: %T", t) diff --git a/pkg/polymorphichelpers/updatepodspec_test.go b/pkg/polymorphichelpers/updatepodspec_test.go index 145de348..f3aa3edd 100644 --- a/pkg/polymorphichelpers/updatepodspec_test.go +++ b/pkg/polymorphichelpers/updatepodspec_test.go @@ -102,6 +102,10 @@ func TestUpdatePodSpecForObject(t *testing.T) { object: &batchv1beta1.CronJob{}, expect: true, }, + { + object: &batchv1.CronJob{}, + expect: true, + }, { object: &v1.Node{}, expect: false,