Skip to content

Commit 41c9572

Browse files
author
Kubernetes Submit Queue
authored
Merge pull request #65463 from smarterclayton/jobs_output
Automatic merge from submit-queue (batch tested with PRs 64575, 65120, 65463, 65434, 65522). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Improve job describe and get output For get, condense completions and success into a single column, and print the job duration. Use a new variant of ShortHumanDuration that shows more significant digits, since duration matters more for jobs. ``` NAME COMPLETIONS DURATION AGE image-mirror-origin-v3.10-1529985600 1/1 47s 42m image-mirror-origin-v3.11-1529985600 1/1 74s 42m image-pruner-1529971200 1/1 60m 4h ``` The completions column can be: ``` COMPLETIONS 0/1 # completions nil or 1, succeeded 0 1/1 # completions nil or 1, succeeded 1 0/3 # completions 3, succeeded 1 1/3 # completions 3, succeeded 1 0/1 of 30 # parallelism of 30, completions is nil ``` Update describe to show the completion time and the duration. ``` Start Time: Mon, 25 Jun 2018 20:00:05 -0400 Completed At: Mon, 25 Jun 2018 21:00:34 -0400 Duration: 60m ``` This is more useful than the current output: ``` NAME DESIRED SUCCESSFUL AGE image-mirror-origin-v3.10-1529982000 1 1 54m image-mirror-origin-v3.11-1529982000 1 1 54m image-pruner-1529971200 1 1 3h ``` ```release-note Improve the display of jobs in `kubectl get` and `kubectl describe` to emphasize progress and duration. ```
2 parents 2a0ad6b + c819a16 commit 41c9572

File tree

6 files changed

+152
-8
lines changed

6 files changed

+152
-8
lines changed

pkg/printers/internalversion/describe.go

+7
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"k8s.io/apimachinery/pkg/labels"
4545
"k8s.io/apimachinery/pkg/runtime/schema"
4646
"k8s.io/apimachinery/pkg/types"
47+
"k8s.io/apimachinery/pkg/util/duration"
4748
"k8s.io/apimachinery/pkg/util/intstr"
4849
"k8s.io/apimachinery/pkg/util/sets"
4950
"k8s.io/client-go/dynamic"
@@ -1855,6 +1856,12 @@ func describeJob(job *batch.Job, events *api.EventList) (string, error) {
18551856
if job.Status.StartTime != nil {
18561857
w.Write(LEVEL_0, "Start Time:\t%s\n", job.Status.StartTime.Time.Format(time.RFC1123Z))
18571858
}
1859+
if job.Status.CompletionTime != nil {
1860+
w.Write(LEVEL_0, "Completed At:\t%s\n", job.Status.CompletionTime.Time.Format(time.RFC1123Z))
1861+
}
1862+
if job.Status.StartTime != nil && job.Status.CompletionTime != nil {
1863+
w.Write(LEVEL_0, "Duration:\t%s\n", duration.HumanDuration(job.Status.CompletionTime.Sub(job.Status.StartTime.Time)))
1864+
}
18581865
if job.Spec.ActiveDeadlineSeconds != nil {
18591866
w.Write(LEVEL_0, "Active Deadline Seconds:\t%ds\n", *job.Spec.ActiveDeadlineSeconds)
18601867
}

pkg/printers/internalversion/printers.go

+21-5
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ func AddHandlers(h printers.PrintHandler) {
151151

152152
jobColumnDefinitions := []metav1beta1.TableColumnDefinition{
153153
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
154-
{Name: "Desired", Type: "integer", Description: batchv1.JobSpec{}.SwaggerDoc()["completions"]},
155-
{Name: "Successful", Type: "integer", Description: batchv1.JobStatus{}.SwaggerDoc()["succeeded"]},
154+
{Name: "Completions", Type: "string", Description: batchv1.JobStatus{}.SwaggerDoc()["succeeded"]},
155+
{Name: "Duration", Type: "string", Description: "Time required to complete the job."},
156156
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
157157
{Name: "Containers", Type: "string", Priority: 1, Description: "Names of each container in the template."},
158158
{Name: "Images", Type: "string", Priority: 1, Description: "Images referenced by each container in the template."},
@@ -760,12 +760,28 @@ func printJob(obj *batch.Job, options printers.PrintOptions) ([]metav1beta1.Tabl
760760

761761
var completions string
762762
if obj.Spec.Completions != nil {
763-
completions = strconv.Itoa(int(*obj.Spec.Completions))
763+
completions = fmt.Sprintf("%d/%d", obj.Status.Succeeded, *obj.Spec.Completions)
764764
} else {
765-
completions = "<none>"
765+
parallelism := int32(0)
766+
if obj.Spec.Parallelism != nil {
767+
parallelism = *obj.Spec.Parallelism
768+
}
769+
if parallelism > 1 {
770+
completions = fmt.Sprintf("%d/1 of %d", obj.Status.Succeeded, parallelism)
771+
} else {
772+
completions = fmt.Sprintf("%d/1", obj.Status.Succeeded)
773+
}
774+
}
775+
var jobDuration string
776+
switch {
777+
case obj.Status.StartTime == nil:
778+
case obj.Status.CompletionTime == nil:
779+
jobDuration = duration.HumanDuration(time.Now().Sub(obj.Status.StartTime.Time))
780+
default:
781+
jobDuration = duration.HumanDuration(obj.Status.CompletionTime.Sub(obj.Status.StartTime.Time))
766782
}
767783

768-
row.Cells = append(row.Cells, obj.Name, completions, int64(obj.Status.Succeeded), translateTimestamp(obj.CreationTimestamp))
784+
row.Cells = append(row.Cells, obj.Name, completions, jobDuration, translateTimestamp(obj.CreationTimestamp))
769785
if options.Wide {
770786
names, images := layoutContainerCells(obj.Spec.Template.Spec.Containers)
771787
row.Cells = append(row.Cells, names, images, metav1.FormatLabelSelector(obj.Spec.Selector))

pkg/printers/internalversion/printers_test.go

+36-2
Original file line numberDiff line numberDiff line change
@@ -2055,6 +2055,7 @@ func TestPrintDaemonSet(t *testing.T) {
20552055
}
20562056

20572057
func TestPrintJob(t *testing.T) {
2058+
now := time.Now()
20582059
completions := int32(2)
20592060
tests := []struct {
20602061
job batch.Job
@@ -2073,7 +2074,7 @@ func TestPrintJob(t *testing.T) {
20732074
Succeeded: 1,
20742075
},
20752076
},
2076-
"job1\t2\t1\t0s\n",
2077+
"job1\t1/2\t\t0s\n",
20772078
},
20782079
{
20792080
batch.Job{
@@ -2088,7 +2089,40 @@ func TestPrintJob(t *testing.T) {
20882089
Succeeded: 0,
20892090
},
20902091
},
2091-
"job2\t<none>\t0\t10y\n",
2092+
"job2\t0/1\t\t10y\n",
2093+
},
2094+
{
2095+
batch.Job{
2096+
ObjectMeta: metav1.ObjectMeta{
2097+
Name: "job3",
2098+
CreationTimestamp: metav1.Time{Time: time.Now().AddDate(-10, 0, 0)},
2099+
},
2100+
Spec: batch.JobSpec{
2101+
Completions: nil,
2102+
},
2103+
Status: batch.JobStatus{
2104+
Succeeded: 0,
2105+
StartTime: &metav1.Time{Time: now.Add(time.Minute)},
2106+
CompletionTime: &metav1.Time{Time: now.Add(31 * time.Minute)},
2107+
},
2108+
},
2109+
"job3\t0/1\t30m\t10y\n",
2110+
},
2111+
{
2112+
batch.Job{
2113+
ObjectMeta: metav1.ObjectMeta{
2114+
Name: "job4",
2115+
CreationTimestamp: metav1.Time{Time: time.Now().AddDate(-10, 0, 0)},
2116+
},
2117+
Spec: batch.JobSpec{
2118+
Completions: nil,
2119+
},
2120+
Status: batch.JobStatus{
2121+
Succeeded: 0,
2122+
StartTime: &metav1.Time{Time: time.Now().Add(-20 * time.Minute)},
2123+
},
2124+
},
2125+
"job4\t0/1\t20m\t10y\n",
20922126
},
20932127
}
20942128

staging/src/k8s.io/apimachinery/pkg/util/duration/BUILD

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("@io_bazel_rules_go//go:def.bzl", "go_library")
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
22

33
go_library(
44
name = "go_default_library",
@@ -21,3 +21,9 @@ filegroup(
2121
tags = ["automanaged"],
2222
visibility = ["//visibility:public"],
2323
)
24+
25+
go_test(
26+
name = "go_default_test",
27+
srcs = ["duration_test.go"],
28+
embed = [":go_default_library"],
29+
)

staging/src/k8s.io/apimachinery/pkg/util/duration/duration.go

+34
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,37 @@ func ShortHumanDuration(d time.Duration) string {
4141
}
4242
return fmt.Sprintf("%dy", int(d.Hours()/24/365))
4343
}
44+
45+
// HumanDuration returns a succint representation of the provided duration
46+
// with limited precision for consumption by humans. It provides ~2-3 significant
47+
// figures of duration.
48+
func HumanDuration(d time.Duration) string {
49+
// Allow deviation no more than 2 seconds(excluded) to tolerate machine time
50+
// inconsistence, it can be considered as almost now.
51+
if seconds := int(d.Seconds()); seconds < -1 {
52+
return fmt.Sprintf("<invalid>")
53+
} else if seconds < 0 {
54+
return fmt.Sprintf("0s")
55+
} else if seconds < 60*2 {
56+
return fmt.Sprintf("%ds", seconds)
57+
}
58+
minutes := int(d / time.Minute)
59+
if minutes < 10 {
60+
return fmt.Sprintf("%dm%ds", minutes, int(d/time.Second)%60)
61+
} else if minutes < 60*3 {
62+
return fmt.Sprintf("%dm", minutes)
63+
}
64+
hours := int(d / time.Hour)
65+
if hours < 8 {
66+
return fmt.Sprintf("%dh%dm", hours, int(d/time.Minute)%60)
67+
} else if hours < 48 {
68+
return fmt.Sprintf("%dh", hours)
69+
} else if hours < 24*8 {
70+
return fmt.Sprintf("%dd%dh", hours/24, hours%24)
71+
} else if hours < 24*365*2 {
72+
return fmt.Sprintf("%dd", hours/24)
73+
} else if hours < 24*365*8 {
74+
return fmt.Sprintf("%dy%dd", hours/24/365, (hours/24)%365)
75+
}
76+
return fmt.Sprintf("%dy", int(hours/24/365))
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
Copyright 2018 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package duration
18+
19+
import (
20+
"testing"
21+
"time"
22+
)
23+
24+
func TestHumanDuration(t *testing.T) {
25+
tests := []struct {
26+
d time.Duration
27+
want string
28+
}{
29+
{d: time.Second, want: "1s"},
30+
{d: 70 * time.Second, want: "70s"},
31+
{d: 190 * time.Second, want: "3m10s"},
32+
{d: 70 * time.Minute, want: "70m"},
33+
{d: 47 * time.Hour, want: "47h"},
34+
{d: 49 * time.Hour, want: "2d1h"},
35+
{d: (8*24 + 2) * time.Hour, want: "8d"},
36+
{d: (367 * 24) * time.Hour, want: "367d"},
37+
{d: (365*2*24 + 25) * time.Hour, want: "2y1d"},
38+
{d: (365*8*24 + 2) * time.Hour, want: "8y"},
39+
}
40+
for _, tt := range tests {
41+
t.Run(tt.d.String(), func(t *testing.T) {
42+
if got := HumanDuration(tt.d); got != tt.want {
43+
t.Errorf("HumanDuration() = %v, want %v", got, tt.want)
44+
}
45+
})
46+
}
47+
}

0 commit comments

Comments
 (0)