diff --git a/pkg/kubectl/cmd/run.go b/pkg/kubectl/cmd/run.go index f534fa8ada3..0074d24807b 100644 --- a/pkg/kubectl/cmd/run.go +++ b/pkg/kubectl/cmd/run.go @@ -69,7 +69,10 @@ var ( kubectl run nginx --image=nginx --command -- ... # Start the perl container to compute π to 2000 places and print it out. - kubectl run pi --image=perl --restart=OnFailure -- perl -Mbignum=bpi -wle 'print bpi(2000)'`) + kubectl run pi --image=perl --restart=OnFailure -- perl -Mbignum=bpi -wle 'print bpi(2000)' + + # Start the scheduled job to compute π to 2000 places and print it out every 5 minutes. + kubectl run pi --schedule="* 0/5 * * * ?" --image=perl --restart=OnFailure -- perl -Mbignum=bpi -wle 'print bpi(2000)'`) ) func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command { @@ -118,6 +121,7 @@ func addRunFlags(cmd *cobra.Command) { cmd.Flags().String("service-generator", "service/v2", "The name of the generator to use for creating a service. Only used if --expose is true") cmd.Flags().String("service-overrides", "", "An inline JSON override for the generated service object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field. Only used if --expose is true.") cmd.Flags().Bool("quiet", false, "If true, suppress prompt messages.") + cmd.Flags().String("schedule", "", "A schedule in the Cron format the job should be run with.") } func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cobra.Command, args []string, argsLenAtDash int) error { @@ -154,6 +158,10 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob } generatorName := cmdutil.GetFlagString(cmd, "generator") + schedule := cmdutil.GetFlagString(cmd, "schedule") + if len(schedule) != 0 && len(generatorName) == 0 { + generatorName = "scheduledjob/v2alpha1" + } if len(generatorName) == 0 { client, err := f.Client() if err != nil { diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index cffaf44741f..b25dbaf125d 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -164,6 +164,7 @@ const ( DeploymentV1Beta1GeneratorName = "deployment/v1beta1" JobV1Beta1GeneratorName = "job/v1beta1" JobV1GeneratorName = "job/v1" + ScheduledJobV2Alpha1GeneratorName = "scheduledjob/v2alpha1" NamespaceV1GeneratorName = "namespace/v1" ResourceQuotaV1GeneratorName = "resourcequotas/v1" SecretV1GeneratorName = "secret/v1" @@ -180,11 +181,12 @@ func DefaultGenerators(cmdName string) map[string]kubectl.Generator { ServiceV2GeneratorName: kubectl.ServiceGeneratorV2{}, } generators["run"] = map[string]kubectl.Generator{ - RunV1GeneratorName: kubectl.BasicReplicationController{}, - RunPodV1GeneratorName: kubectl.BasicPod{}, - DeploymentV1Beta1GeneratorName: kubectl.DeploymentV1Beta1{}, - JobV1Beta1GeneratorName: kubectl.JobV1Beta1{}, - JobV1GeneratorName: kubectl.JobV1{}, + RunV1GeneratorName: kubectl.BasicReplicationController{}, + RunPodV1GeneratorName: kubectl.BasicPod{}, + DeploymentV1Beta1GeneratorName: kubectl.DeploymentV1Beta1{}, + JobV1Beta1GeneratorName: kubectl.JobV1Beta1{}, + JobV1GeneratorName: kubectl.JobV1{}, + ScheduledJobV2Alpha1GeneratorName: kubectl.ScheduledJobV2Alpha1{}, } generators["autoscale"] = map[string]kubectl.Generator{ HorizontalPodAutoscalerV1Beta1GeneratorName: kubectl.HorizontalPodAutoscalerV1Beta1{}, diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index f67e949c8a5..96ec6868435 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -109,6 +109,7 @@ func describerMap(c *client.Client) map[unversioned.GroupKind]Describer { extensions.Kind("Deployment"): &DeploymentDescriber{adapter.FromUnversionedClient(c)}, extensions.Kind("Job"): &JobDescriber{c}, batch.Kind("Job"): &JobDescriber{c}, + batch.Kind("ScheduledJob"): &ScheduledJobDescriber{adapter.FromUnversionedClient(c)}, apps.Kind("PetSet"): &PetSetDescriber{c}, extensions.Kind("Ingress"): &IngressDescriber{c}, } @@ -1019,6 +1020,14 @@ func describeStatus(stateName string, state api.ContainerState, out io.Writer) { } } +func printBoolPtr(value *bool) string { + if value != nil { + return printBool(*value) + } + + return "" +} + func printBool(value bool) string { if value { return "True" @@ -1148,18 +1157,18 @@ func describeReplicaSet(rs *extensions.ReplicaSet, events *api.EventList, runnin // JobDescriber generates information about a job and the pods it has created. type JobDescriber struct { - client *client.Client + client.Interface } func (d *JobDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { - job, err := d.client.Extensions().Jobs(namespace).Get(name) + job, err := d.Batch().Jobs(namespace).Get(name) if err != nil { return "", err } var events *api.EventList if describerSettings.ShowEvents { - events, _ = d.client.Events(namespace).Search(job) + events, _ = d.Events(namespace).Search(job) } return describeJob(job, events) @@ -1194,6 +1203,92 @@ func describeJob(job *batch.Job, events *api.EventList) (string, error) { }) } +// ScheduledJobDescriber generates information about a scheduled job and the jobs it has created. +type ScheduledJobDescriber struct { + clientset.Interface +} + +func (d *ScheduledJobDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { + scheduledJob, err := d.Batch().ScheduledJobs(namespace).Get(name) + if err != nil { + return "", err + } + + var events *api.EventList + if describerSettings.ShowEvents { + events, _ = d.Core().Events(namespace).Search(scheduledJob) + } + + return describeScheduledJob(scheduledJob, events) +} + +func describeScheduledJob(scheduledJob *batch.ScheduledJob, events *api.EventList) (string, error) { + return tabbedString(func(out io.Writer) error { + fmt.Fprintf(out, "Name:\t%s\n", scheduledJob.Name) + fmt.Fprintf(out, "Namespace:\t%s\n", scheduledJob.Namespace) + fmt.Fprintf(out, "Schedule:\t%s\n", scheduledJob.Spec.Schedule) + fmt.Fprintf(out, "Concurrency Policy:\t%s\n", scheduledJob.Spec.ConcurrencyPolicy) + fmt.Fprintf(out, "Suspend:\t%s\n", printBoolPtr(scheduledJob.Spec.Suspend)) + if scheduledJob.Spec.StartingDeadlineSeconds != nil { + fmt.Fprintf(out, "Starting Deadline Seconds:\t%ds\n", *scheduledJob.Spec.StartingDeadlineSeconds) + } else { + fmt.Fprintf(out, "Starting Deadline Seconds:\t\n") + } + describeJobTemplate(scheduledJob.Spec.JobTemplate, out) + printLabelsMultiline(out, "Labels", scheduledJob.Labels) + if scheduledJob.Status.LastScheduleTime != nil { + fmt.Fprintf(out, "Last Schedule Time:\t%s\n", scheduledJob.Status.LastScheduleTime.Time.Format(time.RFC1123Z)) + } else { + fmt.Fprintf(out, "Last Schedule Time:\t\n") + } + printActiveJobs(out, "Active Jobs", scheduledJob.Status.Active) + if events != nil { + DescribeEvents(events, out) + } + return nil + }) +} + +func describeJobTemplate(jobTemplate batch.JobTemplateSpec, out io.Writer) { + fmt.Fprintf(out, "Image(s):\t%s\n", makeImageList(&jobTemplate.Spec.Template.Spec)) + if jobTemplate.Spec.Selector != nil { + selector, _ := unversioned.LabelSelectorAsSelector(jobTemplate.Spec.Selector) + fmt.Fprintf(out, "Selector:\t%s\n", selector) + } else { + fmt.Fprintf(out, "Selector:\t\n") + } + if jobTemplate.Spec.Parallelism != nil { + fmt.Fprintf(out, "Parallelism:\t%d\n", *jobTemplate.Spec.Parallelism) + } else { + fmt.Fprintf(out, "Parallelism:\t\n") + } + if jobTemplate.Spec.Completions != nil { + fmt.Fprintf(out, "Completions:\t%d\n", *jobTemplate.Spec.Completions) + } else { + fmt.Fprintf(out, "Completions:\t\n") + } + if jobTemplate.Spec.ActiveDeadlineSeconds != nil { + fmt.Fprintf(out, "Active Deadline Seconds:\t%ds\n", *jobTemplate.Spec.ActiveDeadlineSeconds) + } + describeVolumes(jobTemplate.Spec.Template.Spec.Volumes, out, "") +} + +func printActiveJobs(out io.Writer, title string, jobs []api.ObjectReference) { + fmt.Fprintf(out, "%s:\t", title) + if len(jobs) == 0 { + fmt.Fprintln(out, "") + return + } + + for i, job := range jobs { + if i != 0 { + fmt.Fprint(out, ", ") + } + fmt.Fprintf(out, "%s", job.Name) + } + fmt.Fprintln(out, "") +} + // DaemonSetDescriber generates information about a daemon set and the pods it has created. type DaemonSetDescriber struct { client.Interface diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 5e4fc1ef081..4631f1e43b1 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -414,6 +414,7 @@ var podTemplateColumns = []string{"TEMPLATE", "CONTAINER(S)", "IMAGE(S)", "PODLA var replicationControllerColumns = []string{"NAME", "DESIRED", "CURRENT", "AGE"} var replicaSetColumns = []string{"NAME", "DESIRED", "CURRENT", "AGE"} var jobColumns = []string{"NAME", "DESIRED", "SUCCESSFUL", "AGE"} +var scheduledJobColumns = []string{"NAME", "SCHEDULE", "SUSPEND", "ACTIVE", "LAST-SCHEDULE"} var serviceColumns = []string{"NAME", "CLUSTER-IP", "EXTERNAL-IP", "PORT(S)", "AGE"} var ingressColumns = []string{"NAME", "HOSTS", "ADDRESS", "PORTS", "AGE"} var petSetColumns = []string{"NAME", "DESIRED", "CURRENT", "AGE"} @@ -459,6 +460,8 @@ func (h *HumanReadablePrinter) addDefaultHandlers() { h.Handler(daemonSetColumns, printDaemonSetList) h.Handler(jobColumns, printJob) h.Handler(jobColumns, printJobList) + h.Handler(scheduledJobColumns, printScheduledJob) + h.Handler(scheduledJobColumns, printScheduledJobList) h.Handler(serviceColumns, printService) h.Handler(serviceColumns, printServiceList) h.Handler(ingressColumns, printIngress) @@ -970,6 +973,42 @@ func printJobList(list *batch.JobList, w io.Writer, options PrintOptions) error return nil } +func printScheduledJob(scheduledJob *batch.ScheduledJob, w io.Writer, options PrintOptions) error { + name := scheduledJob.Name + namespace := scheduledJob.Namespace + + if options.WithNamespace { + if _, err := fmt.Fprintf(w, "%s\t", namespace); err != nil { + return err + } + } + + lastScheduleTime := "" + if scheduledJob.Status.LastScheduleTime != nil { + lastScheduleTime = scheduledJob.Status.LastScheduleTime.Time.Format(time.RFC1123Z) + } + if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s\n", + name, + scheduledJob.Spec.Schedule, + printBoolPtr(scheduledJob.Spec.Suspend), + len(scheduledJob.Status.Active), + lastScheduleTime, + ); err != nil { + return err + } + + return nil +} + +func printScheduledJobList(list *batch.ScheduledJobList, w io.Writer, options PrintOptions) error { + for _, scheduledJob := range list.Items { + if err := printScheduledJob(&scheduledJob, w, options); err != nil { + return err + } + } + return nil +} + // loadBalancerStatusStringer behaves mostly like a string interface and converts the given status to a string. // `wide` indicates whether the returned value is meant for --o=wide output. If not, it's clipped to 16 bytes. func loadBalancerStatusStringer(s api.LoadBalancerStatus, wide bool) string { diff --git a/pkg/kubectl/run.go b/pkg/kubectl/run.go index 0b89b7814f6..76bafe203ce 100644 --- a/pkg/kubectl/run.go +++ b/pkg/kubectl/run.go @@ -27,6 +27,7 @@ import ( "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/apis/batch" batchv1 "k8s.io/kubernetes/pkg/apis/batch/v1" + batchv2alpha1 "k8s.io/kubernetes/pkg/apis/batch/v2alpha1" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/validation" @@ -394,6 +395,104 @@ func (JobV1) Generate(genericParams map[string]interface{}) (runtime.Object, err return &job, nil } +type ScheduledJobV2Alpha1 struct{} + +func (ScheduledJobV2Alpha1) ParamNames() []GeneratorParam { + return []GeneratorParam{ + {"labels", false}, + {"default-name", false}, + {"name", true}, + {"image", true}, + {"port", false}, + {"hostport", false}, + {"stdin", false}, + {"leave-stdin-open", false}, + {"tty", false}, + {"command", false}, + {"args", false}, + {"env", false}, + {"requests", false}, + {"limits", false}, + {"restart", false}, + {"schedule", true}, + } +} + +func (ScheduledJobV2Alpha1) Generate(genericParams map[string]interface{}) (runtime.Object, error) { + args, err := getArgs(genericParams) + if err != nil { + return nil, err + } + + envs, err := getV1Envs(genericParams) + if err != nil { + return nil, err + } + + params, err := getParams(genericParams) + if err != nil { + return nil, err + } + + name, err := getName(params) + if err != nil { + return nil, err + } + + labels, err := getLabels(params, true, name) + if err != nil { + return nil, err + } + + podSpec, err := makeV1PodSpec(params, name) + if err != nil { + return nil, err + } + + if err = updateV1PodContainers(params, args, envs, podSpec); err != nil { + return nil, err + } + + leaveStdinOpen, err := GetBool(params, "leave-stdin-open", false) + if err != nil { + return nil, err + } + podSpec.Containers[0].StdinOnce = !leaveStdinOpen && podSpec.Containers[0].Stdin + + if err := updateV1PodPorts(params, podSpec); err != nil { + return nil, err + } + + restartPolicy := v1.RestartPolicy(params["restart"]) + if len(restartPolicy) == 0 { + restartPolicy = v1.RestartPolicyNever + } + podSpec.RestartPolicy = restartPolicy + + scheduledJob := batchv2alpha1.ScheduledJob{ + ObjectMeta: v1.ObjectMeta{ + Name: name, + Labels: labels, + }, + Spec: batchv2alpha1.ScheduledJobSpec{ + Schedule: params["schedule"], + ConcurrencyPolicy: batchv2alpha1.AllowConcurrent, + JobTemplate: batchv2alpha1.JobTemplateSpec{ + Spec: batchv2alpha1.JobSpec{ + Template: v1.PodTemplateSpec{ + ObjectMeta: v1.ObjectMeta{ + Labels: labels, + }, + Spec: *podSpec, + }, + }, + }, + }, + } + + return &scheduledJob, nil +} + type BasicReplicationController struct{} func (BasicReplicationController) ParamNames() []GeneratorParam {