mirror of https://github.com/k3s-io/k3s
Fixes #47538: Add functionality for manually creating a Job instance from a CronJob
This changeset adds the command `kubectl create job` with the flag `--from-cronjob`, which allows a user to create a Job from a CronJob via the CLI.pull/6/head
parent
270148d7d9
commit
5b57c2db00
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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",
|
||||
|
@ -132,6 +133,7 @@ go_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/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/strategicpatch:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/validation:go_default_library",
|
||||
|
@ -142,6 +144,8 @@ 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/batch/v1beta1: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 +175,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 +236,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 +259,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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
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"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
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"
|
||||
clientbatchv1beta1 "k8s.io/client-go/kubernetes/typed/batch/v1beta1"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
jobLong = templates.LongDesc(i18n.T(`
|
||||
Create a Job to execute immediately from a specified CronJob.`))
|
||||
|
||||
jobExample = templates.Examples(i18n.T(`
|
||||
# Create a Job from a CronJob named "a-cronjob"
|
||||
kubectl create job --from-cronjob=a-cronjob`))
|
||||
)
|
||||
|
||||
type CreateJobOptions struct {
|
||||
FromCronJob string
|
||||
|
||||
OutputFormat string
|
||||
Namespace string
|
||||
V1Beta1Client clientbatchv1beta1.BatchV1beta1Interface
|
||||
V1Client clientbatchv1.BatchV1Interface
|
||||
Mapper meta.RESTMapper
|
||||
Out io.Writer
|
||||
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.
|
||||
func NewCmdCreateJob(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command {
|
||||
c := &CreateJobOptions{
|
||||
Out: cmdOut,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "job --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-cronjob", "", "Specify the name of the CronJob to create a Job from.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *CreateJobOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) (err error) {
|
||||
c.FromCronJob = cmdutil.GetFlagString(cmd, "from-cronjob")
|
||||
|
||||
// Complete other options for Run.
|
||||
c.Mapper, _ = f.Object()
|
||||
|
||||
c.OutputFormat = cmdutil.GetFlagString(cmd, "output")
|
||||
|
||||
c.Namespace, _, err = f.DefaultNamespace()
|
||||
if err != nil {
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.V1Client == nil {
|
||||
c.V1Client = clientset.BatchV1()
|
||||
}
|
||||
if c.V1Beta1Client == nil {
|
||||
c.V1Beta1Client = clientset.BatchV1beta1()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CreateJobOptions) RunCreateJob() (err error) {
|
||||
cronjob, err := c.V1Beta1Client.CronJobs(c.Namespace).Get(c.FromCronJob, metav1.GetOptions{})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch job: %v", err)
|
||||
}
|
||||
|
||||
annotations := make(map[string]string)
|
||||
annotations["cronjob.kubernetes.io/instantiate"] = "manual"
|
||||
|
||||
labels := make(map[string]string)
|
||||
for k, v := range cronjob.Spec.JobTemplate.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
|
||||
jobToCreate := &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
// job name cannot exceed DNS1053LabelMaxLength (52 characters)
|
||||
Name: cronjob.Name + "-manual-" + rand.String(3),
|
||||
Namespace: c.Namespace,
|
||||
Annotations: annotations,
|
||||
Labels: labels,
|
||||
},
|
||||
Spec: cronjob.Spec.JobTemplate.Spec,
|
||||
}
|
||||
|
||||
result, err := c.V1Client.Jobs(c.Namespace).Create(jobToCreate)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create job: %v", err)
|
||||
}
|
||||
|
||||
return c.PrintObject(result)
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
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"
|
||||
fake "k8s.io/client-go/kubernetes/fake"
|
||||
clienttesting "k8s.io/client-go/testing"
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
)
|
||||
|
||||
var submittedJob *batchv1.Job
|
||||
|
||||
func TestCreateJobFromCronJob(t *testing.T) {
|
||||
testNamespaceName := "test"
|
||||
testCronJobName := "test-cronjob"
|
||||
testImageName := "fake"
|
||||
|
||||
expectedLabels := make(map[string]string)
|
||||
expectedAnnotations := make(map[string]string)
|
||||
expectedLabels["test-label"] = "test-value"
|
||||
expectedAnnotations["cronjob.kubernetes.io/instantiate"] = "manual"
|
||||
|
||||
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},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cronJobToCreate := &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("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) {
|
||||
ca := action.(clienttesting.CreateAction)
|
||||
submittedJob = ca.GetObject().(*batchv1.Job)
|
||||
return true, expectJob, nil
|
||||
})
|
||||
|
||||
f, _, _, _ := cmdtesting.NewAPIFactory()
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
|
||||
mapper, _ := f.Object()
|
||||
cmdOptions := &CreateJobOptions{
|
||||
Out: buf,
|
||||
OutputFormat: "na",
|
||||
Namespace: testNamespaceName,
|
||||
FromCronJob: testCronJobName,
|
||||
V1Client: clientset.BatchV1(),
|
||||
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)
|
||||
if annotationsArrayLength != 1 {
|
||||
t.Errorf("expected length of annotations array to be 1, got %d", annotationsArrayLength)
|
||||
}
|
||||
|
||||
if v, ok := submittedJob.Annotations["cronjob.kubernetes.io/instantiate"]; ok {
|
||||
if v != "manual" {
|
||||
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 labelsArrayLength != 1 {
|
||||
t.Errorf("expected length of labels array to be 1, got %d", labelsArrayLength)
|
||||
}
|
||||
|
||||
if v, ok := submittedJob.Labels["test-label"]; ok {
|
||||
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 containerArrayLength != 1 {
|
||||
t.Errorf("expected length of container array to be 1, got %d", containerArrayLength)
|
||||
}
|
||||
|
||||
if submittedJob.Spec.Template.Spec.Containers[0].Image != testImageName {
|
||||
t.Errorf("expected '%s', got '%s'", testImageName, submittedJob.Spec.Template.Spec.Containers[0].Image)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue