// SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s package render import ( "fmt" "strconv" "strings" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/model1" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/duration" ) var defaultJOBHeader = model1.Header{ model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, model1.HeaderColumn{Name: "COMPLETIONS"}, model1.HeaderColumn{Name: "DURATION"}, model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}}, model1.HeaderColumn{Name: "CONTAINERS", Attrs: model1.Attrs{Wide: true}}, model1.HeaderColumn{Name: "IMAGES", Attrs: model1.Attrs{Wide: true}}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, } // Job renders a K8s Job to screen. type Job struct { Base } // Header returns a header row. func (j Job) Header(_ string) model1.Header { return j.doHeader(defaultJOBHeader) } // Render renders a K8s resource to screen. func (j Job) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected Unstructured, but got %T", o) } if err := j.defaultRow(raw, row); err != nil { return err } if j.specs.isEmpty() { return nil } cols, err := j.specs.realize(raw, defaultJOBHeader, row) if err != nil { return err } cols.hydrateRow(row) return nil } func (j Job) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error { var job batchv1.Job err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &job) if err != nil { return err } ready := toCompletion(&job.Spec, &job.Status) cc, ii := toContainers(&job.Spec.Template.Spec) r.ID = client.MetaFQN(&job.ObjectMeta) r.Fields = model1.Fields{ job.Namespace, job.Name, computeVulScore(job.Namespace, job.Labels, &job.Spec.Template.Spec), ready, toDuration(&job.Status), jobSelector(&job.Spec), cc, ii, AsStatus(j.diagnose(ready, &job.Status)), ToAge(job.GetCreationTimestamp()), } return nil } func (Job) diagnose(ready string, status *batchv1.JobStatus) error { tokens := strings.Split(ready, "/") if tokens[0] != tokens[1] && status.Failed > 0 { return fmt.Errorf("%d pods failed", status.Failed) } return nil } // ---------------------------------------------------------------------------- // Helpers... const maxShow = 2 func toContainers(p *v1.PodSpec) (containers, images string) { cc, ii := parseContainers(p.InitContainers) cn, ci := parseContainers(p.Containers) cc, ii = append(cc, cn...), append(ii, ci...) // Limit to 2 of each... if len(cc) > maxShow { cc = append(cc[:2], "(+"+strconv.Itoa(len(cc)-maxShow)+")...") } if len(ii) > maxShow { ii = append(ii[:2], "(+"+strconv.Itoa(len(ii)-maxShow)+")...") } return strings.Join(cc, ","), strings.Join(ii, ",") } func parseContainers(cos []v1.Container) (nn, ii []string) { nn, ii = make([]string, 0, len(cos)), make([]string, 0, len(cos)) for i := range cos { nn, ii = append(nn, cos[i].Name), append(ii, cos[i].Image) } return nn, ii } func toCompletion(spec *batchv1.JobSpec, status *batchv1.JobStatus) (s string) { if spec.Completions != nil { return strconv.Itoa(int(status.Succeeded)) + "/" + strconv.Itoa(int(*spec.Completions)) } if spec.Parallelism == nil { return strconv.Itoa(int(status.Succeeded)) + "/1" } p := *spec.Parallelism if p > 1 { return strconv.Itoa(int(status.Succeeded)) + "/1 of " + strconv.Itoa(int(p)) } return strconv.Itoa(int(status.Succeeded)) + "/1" } func toDuration(status *batchv1.JobStatus) string { if status.StartTime == nil || status.CompletionTime == nil { return MissingValue } return duration.HumanDuration(status.CompletionTime.Sub(status.StartTime.Time)) }