mirror of https://github.com/k3s-io/k3s
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.pull/8/head
parent
93055c7730
commit
c819a16284
|
@ -44,6 +44,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/duration"
|
||||||
"k8s.io/apimachinery/pkg/util/intstr"
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
|
@ -1855,6 +1856,12 @@ func describeJob(job *batch.Job, events *api.EventList) (string, error) {
|
||||||
if job.Status.StartTime != nil {
|
if job.Status.StartTime != nil {
|
||||||
w.Write(LEVEL_0, "Start Time:\t%s\n", job.Status.StartTime.Time.Format(time.RFC1123Z))
|
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 {
|
if job.Spec.ActiveDeadlineSeconds != nil {
|
||||||
w.Write(LEVEL_0, "Active Deadline Seconds:\t%ds\n", *job.Spec.ActiveDeadlineSeconds)
|
w.Write(LEVEL_0, "Active Deadline Seconds:\t%ds\n", *job.Spec.ActiveDeadlineSeconds)
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,8 +149,8 @@ func AddHandlers(h printers.PrintHandler) {
|
||||||
|
|
||||||
jobColumnDefinitions := []metav1beta1.TableColumnDefinition{
|
jobColumnDefinitions := []metav1beta1.TableColumnDefinition{
|
||||||
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
|
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
|
||||||
{Name: "Desired", Type: "integer", Description: batchv1.JobSpec{}.SwaggerDoc()["completions"]},
|
{Name: "Completions", Type: "string", Description: batchv1.JobStatus{}.SwaggerDoc()["succeeded"]},
|
||||||
{Name: "Successful", Type: "integer", 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: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
|
||||||
{Name: "Containers", Type: "string", Priority: 1, Description: "Names of each container in the template."},
|
{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."},
|
{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
|
var completions string
|
||||||
if obj.Spec.Completions != nil {
|
if obj.Spec.Completions != nil {
|
||||||
completions = strconv.Itoa(int(*obj.Spec.Completions))
|
completions = fmt.Sprintf("%d/%d", obj.Status.Succeeded, *obj.Spec.Completions)
|
||||||
} else {
|
} else {
|
||||||
completions = "<none>"
|
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 {
|
if options.Wide {
|
||||||
names, images := layoutContainerCells(obj.Spec.Template.Spec.Containers)
|
names, images := layoutContainerCells(obj.Spec.Template.Spec.Containers)
|
||||||
row.Cells = append(row.Cells, names, images, metav1.FormatLabelSelector(obj.Spec.Selector))
|
row.Cells = append(row.Cells, names, images, metav1.FormatLabelSelector(obj.Spec.Selector))
|
||||||
|
|
|
@ -2054,6 +2054,7 @@ func TestPrintDaemonSet(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrintJob(t *testing.T) {
|
func TestPrintJob(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
completions := int32(2)
|
completions := int32(2)
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
job batch.Job
|
job batch.Job
|
||||||
|
@ -2072,7 +2073,7 @@ func TestPrintJob(t *testing.T) {
|
||||||
Succeeded: 1,
|
Succeeded: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"job1\t2\t1\t0s\n",
|
"job1\t1/2\t\t0s\n",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
batch.Job{
|
batch.Job{
|
||||||
|
@ -2087,7 +2088,40 @@ func TestPrintJob(t *testing.T) {
|
||||||
Succeeded: 0,
|
Succeeded: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"job2\t<none>\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",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
go_library(
|
||||||
name = "go_default_library",
|
name = "go_default_library",
|
||||||
|
@ -21,3 +21,9 @@ filegroup(
|
||||||
tags = ["automanaged"],
|
tags = ["automanaged"],
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "go_default_test",
|
||||||
|
srcs = ["duration_test.go"],
|
||||||
|
embed = [":go_default_library"],
|
||||||
|
)
|
||||||
|
|
|
@ -41,3 +41,37 @@ func ShortHumanDuration(d time.Duration) string {
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%dy", int(d.Hours()/24/365))
|
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("<invalid>")
|
||||||
|
} 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))
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue