From c819a162840f4c36427b7f571d01873734e87c87 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Tue, 26 Jun 2018 00:51:16 -0400 Subject: [PATCH] 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. --- pkg/printers/internalversion/describe.go | 7 +++ pkg/printers/internalversion/printers.go | 26 ++++++++-- pkg/printers/internalversion/printers_test.go | 38 ++++++++++++++- .../apimachinery/pkg/util/duration/BUILD | 8 +++- .../pkg/util/duration/duration.go | 34 ++++++++++++++ .../pkg/util/duration/duration_test.go | 47 +++++++++++++++++++ 6 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 staging/src/k8s.io/apimachinery/pkg/util/duration/duration_test.go diff --git a/pkg/printers/internalversion/describe.go b/pkg/printers/internalversion/describe.go index 9493b565f43..d9ecbb6d12f 100644 --- a/pkg/printers/internalversion/describe.go +++ b/pkg/printers/internalversion/describe.go @@ -44,6 +44,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/duration" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/dynamic" @@ -1855,6 +1856,12 @@ func describeJob(job *batch.Job, events *api.EventList) (string, error) { if job.Status.StartTime != nil { w.Write(LEVEL_0, "Start Time:\t%s\n", job.Status.StartTime.Time.Format(time.RFC1123Z)) } + if job.Status.CompletionTime != nil { + w.Write(LEVEL_0, "Completed At:\t%s\n", job.Status.CompletionTime.Time.Format(time.RFC1123Z)) + } + if job.Status.StartTime != nil && job.Status.CompletionTime != nil { + w.Write(LEVEL_0, "Duration:\t%s\n", duration.HumanDuration(job.Status.CompletionTime.Sub(job.Status.StartTime.Time))) + } if job.Spec.ActiveDeadlineSeconds != nil { w.Write(LEVEL_0, "Active Deadline Seconds:\t%ds\n", *job.Spec.ActiveDeadlineSeconds) } diff --git a/pkg/printers/internalversion/printers.go b/pkg/printers/internalversion/printers.go index 1a804d08fcb..71864f4d082 100644 --- a/pkg/printers/internalversion/printers.go +++ b/pkg/printers/internalversion/printers.go @@ -149,8 +149,8 @@ func AddHandlers(h printers.PrintHandler) { jobColumnDefinitions := []metav1beta1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, - {Name: "Desired", Type: "integer", Description: batchv1.JobSpec{}.SwaggerDoc()["completions"]}, - {Name: "Successful", Type: "integer", Description: batchv1.JobStatus{}.SwaggerDoc()["succeeded"]}, + {Name: "Completions", Type: "string", Description: batchv1.JobStatus{}.SwaggerDoc()["succeeded"]}, + {Name: "Duration", Type: "string", Description: "Time required to complete the job."}, {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, {Name: "Containers", Type: "string", Priority: 1, Description: "Names of each container in the template."}, {Name: "Images", Type: "string", Priority: 1, Description: "Images referenced by each container in the template."}, @@ -750,12 +750,28 @@ func printJob(obj *batch.Job, options printers.PrintOptions) ([]metav1beta1.Tabl var completions string if obj.Spec.Completions != nil { - completions = strconv.Itoa(int(*obj.Spec.Completions)) + completions = fmt.Sprintf("%d/%d", obj.Status.Succeeded, *obj.Spec.Completions) } else { - completions = "" + parallelism := int32(0) + if obj.Spec.Parallelism != nil { + parallelism = *obj.Spec.Parallelism + } + if parallelism > 1 { + completions = fmt.Sprintf("%d/1 of %d", obj.Status.Succeeded, parallelism) + } else { + completions = fmt.Sprintf("%d/1", obj.Status.Succeeded) + } + } + var jobDuration string + switch { + case obj.Status.StartTime == nil: + case obj.Status.CompletionTime == nil: + jobDuration = duration.HumanDuration(time.Now().Sub(obj.Status.StartTime.Time)) + default: + jobDuration = duration.HumanDuration(obj.Status.CompletionTime.Sub(obj.Status.StartTime.Time)) } - row.Cells = append(row.Cells, obj.Name, completions, int64(obj.Status.Succeeded), translateTimestamp(obj.CreationTimestamp)) + row.Cells = append(row.Cells, obj.Name, completions, jobDuration, translateTimestamp(obj.CreationTimestamp)) if options.Wide { names, images := layoutContainerCells(obj.Spec.Template.Spec.Containers) row.Cells = append(row.Cells, names, images, metav1.FormatLabelSelector(obj.Spec.Selector)) diff --git a/pkg/printers/internalversion/printers_test.go b/pkg/printers/internalversion/printers_test.go index ee612d19cc2..d0dce588145 100644 --- a/pkg/printers/internalversion/printers_test.go +++ b/pkg/printers/internalversion/printers_test.go @@ -2054,6 +2054,7 @@ func TestPrintDaemonSet(t *testing.T) { } func TestPrintJob(t *testing.T) { + now := time.Now() completions := int32(2) tests := []struct { job batch.Job @@ -2072,7 +2073,7 @@ func TestPrintJob(t *testing.T) { Succeeded: 1, }, }, - "job1\t2\t1\t0s\n", + "job1\t1/2\t\t0s\n", }, { batch.Job{ @@ -2087,7 +2088,40 @@ func TestPrintJob(t *testing.T) { Succeeded: 0, }, }, - "job2\t\t0\t10y\n", + "job2\t0/1\t\t10y\n", + }, + { + batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "job3", + CreationTimestamp: metav1.Time{Time: time.Now().AddDate(-10, 0, 0)}, + }, + Spec: batch.JobSpec{ + Completions: nil, + }, + Status: batch.JobStatus{ + Succeeded: 0, + StartTime: &metav1.Time{Time: now.Add(time.Minute)}, + CompletionTime: &metav1.Time{Time: now.Add(31 * time.Minute)}, + }, + }, + "job3\t0/1\t30m\t10y\n", + }, + { + batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "job4", + CreationTimestamp: metav1.Time{Time: time.Now().AddDate(-10, 0, 0)}, + }, + Spec: batch.JobSpec{ + Completions: nil, + }, + Status: batch.JobStatus{ + Succeeded: 0, + StartTime: &metav1.Time{Time: time.Now().Add(-20 * time.Minute)}, + }, + }, + "job4\t0/1\t20m\t10y\n", }, } diff --git a/staging/src/k8s.io/apimachinery/pkg/util/duration/BUILD b/staging/src/k8s.io/apimachinery/pkg/util/duration/BUILD index 9ba4d78e110..98f515dadbb 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/duration/BUILD +++ b/staging/src/k8s.io/apimachinery/pkg/util/duration/BUILD @@ -1,4 +1,4 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", @@ -21,3 +21,9 @@ filegroup( tags = ["automanaged"], visibility = ["//visibility:public"], ) + +go_test( + name = "go_default_test", + srcs = ["duration_test.go"], + embed = [":go_default_library"], +) diff --git a/staging/src/k8s.io/apimachinery/pkg/util/duration/duration.go b/staging/src/k8s.io/apimachinery/pkg/util/duration/duration.go index 00404c6cdda..0b88ab6c1fe 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/duration/duration.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/duration/duration.go @@ -41,3 +41,37 @@ func ShortHumanDuration(d time.Duration) string { } return fmt.Sprintf("%dy", int(d.Hours()/24/365)) } + +// HumanDuration returns a succint representation of the provided duration +// with limited precision for consumption by humans. It provides ~2-3 significant +// figures of duration. +func HumanDuration(d time.Duration) string { + // Allow deviation no more than 2 seconds(excluded) to tolerate machine time + // inconsistence, it can be considered as almost now. + if seconds := int(d.Seconds()); seconds < -1 { + return fmt.Sprintf("") + } else if seconds < 0 { + return fmt.Sprintf("0s") + } else if seconds < 60*2 { + return fmt.Sprintf("%ds", seconds) + } + minutes := int(d / time.Minute) + if minutes < 10 { + return fmt.Sprintf("%dm%ds", minutes, int(d/time.Second)%60) + } else if minutes < 60*3 { + return fmt.Sprintf("%dm", minutes) + } + hours := int(d / time.Hour) + if hours < 8 { + return fmt.Sprintf("%dh%dm", hours, int(d/time.Minute)%60) + } else if hours < 48 { + return fmt.Sprintf("%dh", hours) + } else if hours < 24*8 { + return fmt.Sprintf("%dd%dh", hours/24, hours%24) + } else if hours < 24*365*2 { + return fmt.Sprintf("%dd", hours/24) + } else if hours < 24*365*8 { + return fmt.Sprintf("%dy%dd", hours/24/365, (hours/24)%365) + } + return fmt.Sprintf("%dy", int(hours/24/365)) +} diff --git a/staging/src/k8s.io/apimachinery/pkg/util/duration/duration_test.go b/staging/src/k8s.io/apimachinery/pkg/util/duration/duration_test.go new file mode 100644 index 00000000000..f11d5386ccd --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/util/duration/duration_test.go @@ -0,0 +1,47 @@ +/* +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 duration + +import ( + "testing" + "time" +) + +func TestHumanDuration(t *testing.T) { + tests := []struct { + d time.Duration + want string + }{ + {d: time.Second, want: "1s"}, + {d: 70 * time.Second, want: "70s"}, + {d: 190 * time.Second, want: "3m10s"}, + {d: 70 * time.Minute, want: "70m"}, + {d: 47 * time.Hour, want: "47h"}, + {d: 49 * time.Hour, want: "2d1h"}, + {d: (8*24 + 2) * time.Hour, want: "8d"}, + {d: (367 * 24) * time.Hour, want: "367d"}, + {d: (365*2*24 + 25) * time.Hour, want: "2y1d"}, + {d: (365*8*24 + 2) * time.Hour, want: "8y"}, + } + for _, tt := range tests { + t.Run(tt.d.String(), func(t *testing.T) { + if got := HumanDuration(tt.d); got != tt.want { + t.Errorf("HumanDuration() = %v, want %v", got, tt.want) + } + }) + } +}