235 lines
5.2 KiB
Go
235 lines
5.2 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright Authors of K9s
|
|
|
|
package render
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/derailed/k9s/internal/model1"
|
|
"github.com/derailed/k9s/internal/slogs"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/client-go/util/jsonpath"
|
|
)
|
|
|
|
// ColsSpecs represents a collection of column specification ie NAME:spec|flags.
|
|
type ColsSpecs []string
|
|
|
|
// NewColsSpecs returns a new instance.
|
|
func NewColsSpecs(cols ...string) ColsSpecs {
|
|
return ColsSpecs(cols)
|
|
}
|
|
|
|
func (cc ColsSpecs) parseSpecs() (ColumnSpecs, error) {
|
|
specs := make(ColumnSpecs, 0, len(cc))
|
|
|
|
for _, c := range cc {
|
|
def, err := parse(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
specs = append(specs, ColumnSpec{
|
|
Header: def.toHeaderCol(),
|
|
Spec: def.spec,
|
|
})
|
|
}
|
|
|
|
return specs, nil
|
|
}
|
|
|
|
// RenderedCols tracks a collection of column header and cust column parse expression.
|
|
type RenderedCols []RenderedCol
|
|
|
|
func (rr RenderedCols) hydrateRow(row *model1.Row) {
|
|
ff := make(model1.Fields, 0, len(row.Fields))
|
|
for _, c := range rr {
|
|
ff = append(ff, c.Value)
|
|
}
|
|
row.Fields = ff
|
|
}
|
|
|
|
// HasHeader checks if a given header is present in the collection.
|
|
func (rr RenderedCols) HasHeader(n string) bool {
|
|
for _, r := range rr {
|
|
if r.has(n) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// RenderedCol represents a column header and a column spec.
|
|
type RenderedCol struct {
|
|
Header model1.HeaderColumn
|
|
Value string
|
|
}
|
|
|
|
// Has checks if the header column match the given name.
|
|
func (r RenderedCol) has(n string) bool {
|
|
return r.Header.Name == n
|
|
}
|
|
|
|
// ColumnSpec tracks a header column and an options cust column spec.
|
|
type ColumnSpec struct {
|
|
Header model1.HeaderColumn
|
|
Spec string
|
|
}
|
|
|
|
// ColumnSpecs tracks a collection of column specs.
|
|
type ColumnSpecs []ColumnSpec
|
|
|
|
func (c ColumnSpecs) isEmpty() bool {
|
|
return len(c) == 0
|
|
}
|
|
|
|
// Header builds a new header that is a super set of custom and/or default header.
|
|
func (cc ColumnSpecs) Header(rh model1.Header) model1.Header {
|
|
hh := make(model1.Header, 0, len(cc))
|
|
for _, h := range cc {
|
|
hh = append(hh, h.Header)
|
|
}
|
|
|
|
for _, h := range rh {
|
|
if idx, ok := hh.IndexOf(h.Name, true); ok {
|
|
hh[idx].Attrs = hh[idx].Merge(h.Attrs)
|
|
continue
|
|
}
|
|
hh = append(hh, h)
|
|
}
|
|
|
|
return hh
|
|
}
|
|
|
|
func (cc ColumnSpecs) realize(o runtime.Object, rh model1.Header, row *model1.Row) (RenderedCols, error) {
|
|
parsers := make([]*jsonpath.JSONPath, len(cc))
|
|
for ix := range cc {
|
|
if cc[ix].Spec == "" {
|
|
parsers[ix] = nil
|
|
continue
|
|
}
|
|
parsers[ix] = jsonpath.New(
|
|
fmt.Sprintf("column%d", ix),
|
|
).AllowMissingKeys(true)
|
|
if err := parsers[ix].Parse(cc[ix].Spec); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
vv, err := hydrate(o, cc, parsers, rh, row)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, hc := range rh {
|
|
if vv.HasHeader(hc.Name) {
|
|
continue
|
|
}
|
|
if idx, ok := rh.IndexOf(hc.Name, true); ok {
|
|
rc := RenderedCol{Header: hc, Value: row.Fields[idx]}
|
|
rc.Header.Wide = true
|
|
vv = append(vv, rc)
|
|
}
|
|
}
|
|
|
|
return vv, nil
|
|
}
|
|
|
|
func hydrate(o runtime.Object, cc ColumnSpecs, parsers []*jsonpath.JSONPath, rh model1.Header, row *model1.Row) (RenderedCols, error) {
|
|
cols := make(RenderedCols, len(parsers))
|
|
for idx := range parsers {
|
|
parser := parsers[idx]
|
|
if parser == nil {
|
|
ix, ok := rh.IndexOf(cc[idx].Header.Name, true)
|
|
if !ok {
|
|
cols[idx] = RenderedCol{
|
|
Header: cc[idx].Header,
|
|
Value: NAValue,
|
|
}
|
|
slog.Warn("Unable to find custom column", slogs.Name, cc[idx].Header.Name)
|
|
continue
|
|
}
|
|
var v string
|
|
if ix >= len(row.Fields) {
|
|
v = NAValue
|
|
} else {
|
|
v = row.Fields[ix]
|
|
}
|
|
cols[idx] = RenderedCol{
|
|
Header: rh[ix],
|
|
Value: v,
|
|
}
|
|
continue
|
|
}
|
|
|
|
var (
|
|
vals [][]reflect.Value
|
|
err error
|
|
)
|
|
if unstructured, ok := o.(runtime.Unstructured); ok {
|
|
vals, err = parser.FindResults(unstructured.UnstructuredContent())
|
|
} else {
|
|
vals, err = parser.FindResults(reflect.ValueOf(o).Elem().Interface())
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values := make([]string, 0, len(vals))
|
|
if len(vals) == 0 || len(vals[0]) == 0 {
|
|
values = append(values, MissingValue)
|
|
}
|
|
for i := range vals {
|
|
for j := range vals[i] {
|
|
var (
|
|
strVal string
|
|
v = vals[i][j].Interface()
|
|
)
|
|
switch {
|
|
case cc[idx].Header.MXC:
|
|
switch k := v.(type) {
|
|
case resource.Quantity:
|
|
strVal = toMc(k.MilliValue())
|
|
case string:
|
|
if q, err := resource.ParseQuantity(k); err == nil {
|
|
strVal = toMc(q.MilliValue())
|
|
}
|
|
}
|
|
case cc[idx].Header.MXM:
|
|
switch k := v.(type) {
|
|
case resource.Quantity:
|
|
strVal = toMi(k.MilliValue())
|
|
case string:
|
|
if q, err := resource.ParseQuantity(k); err == nil {
|
|
strVal = toMi(q.MilliValue())
|
|
}
|
|
}
|
|
case cc[idx].Header.Time:
|
|
switch k := v.(type) {
|
|
case string:
|
|
if t, err := time.Parse(time.RFC3339, k); err == nil {
|
|
strVal = ToAge(metav1.Time{Time: t})
|
|
}
|
|
case metav1.Time:
|
|
strVal = ToAge(k)
|
|
}
|
|
}
|
|
if strVal == "" {
|
|
strVal = fmt.Sprintf("%v", v)
|
|
}
|
|
values = append(values, strVal)
|
|
}
|
|
}
|
|
cols[idx] = RenderedCol{
|
|
Header: cc[idx].Header,
|
|
Value: strings.Join(values, ","),
|
|
}
|
|
}
|
|
|
|
return cols, nil
|
|
}
|