Rel v0.40.0 (#3109)

* misc cleanups

* update vul scans

* update deps

* [custv] config updates

* [custv] dao updates

* [custv] model updates

* [custv] render updates

* [custv] ui updates

* rel notes
mine
Fernand Galiana 2025-02-15 15:33:33 -07:00 committed by GitHub
parent bccea4bf09
commit c45c44c111
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
88 changed files with 2590 additions and 778 deletions

1
.gitignore vendored
View File

@ -24,3 +24,4 @@ demos
kind kind
*.snap *.snap
/stresser /stresser
__debug_bin*

View File

@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:
else else
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
endif endif
VERSION ?= v0.32.7 VERSION ?= v0.40.0
IMG_NAME := derailed/k9s IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION} IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -627,7 +627,25 @@ The annotation value must specify a container to forward to as well as a local p
You can change which columns shows up for a given resource via custom views. To surface this feature, you will need to create a new configuration file, namely `$XDG_CONFIG_HOME/k9s/views.yaml`. This file leverages GVR (Group/Version/Resource) to configure the associated table view columns. If no GVR is found for a view the default rendering will take over (ie what we have now). Going wide will add all the remaining columns that are available on the given resource after your custom columns. To boot, you can edit your views config file and tune your resources views live! You can change which columns shows up for a given resource via custom views. To surface this feature, you will need to create a new configuration file, namely `$XDG_CONFIG_HOME/k9s/views.yaml`. This file leverages GVR (Group/Version/Resource) to configure the associated table view columns. If no GVR is found for a view the default rendering will take over (ie what we have now). Going wide will add all the remaining columns that are available on the given resource after your custom columns. To boot, you can edit your views config file and tune your resources views live!
> NOTE: This is experimental and will most likely change as we iron this out! 📢 🎉 As of `release v0.40.0` you can specify json parse expressions to further customize your resources rendering.
The new column syntax is as follows:
> COLUMN_NAME<:json_parse_expression><|column_attributes>
Where `:json_parse_expression` represents an expression to pull a specific snippet out of the resource manifest.
Similar to `kubectl -o custom-columns` command. This expression is optional.
Additionally, you can specify column attributes to further tailor the column rendering.
To use this you will need to add a `|` indicator followed by your rendering bits.
You can have one or more of the following attributes:
* `T` -> time column indicator
* `N` -> number column indicator
* `W` -> turns on wide column aka only shows while in wide mode. Defaults to the standard resource definition when present.
* `H` -> Hides the column
* `L` -> Left align (default)
* `R` -> Right align
Here is a sample views configuration that customize a pods and services views. Here is a sample views configuration that customize a pods and services views.
@ -637,7 +655,9 @@ views:
v1/pods: v1/pods:
columns: columns:
- AGE - AGE
- NAMESPACE - NAMESPACE|WR # => 🌚 Specifies the NAMESPACE column to be right aligned and only visible while in wide mode
- ZORG:.metadata.labels.fred\.io\.kubernetes\.blee # => 🌚 extract fred.io.kubernetes.blee label into it's own column
- BLEE:.metadata.annotations.blee|R # => 🌚 extract annotation blee into it's own column and right align it
- NAME - NAME
- IP - IP
- NODE - NODE
@ -652,6 +672,8 @@ views:
- CLUSTER-IP - CLUSTER-IP
``` ```
> 🩻 NOTE: This is experimental and will most likely change as we iron this out!
--- ---
## Plugins ## Plugins

View File

@ -0,0 +1,177 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# Release v0.40.0
## Notes
Thank you to all that contributed with flushing out issues and enhancements for K9s!
I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev
and see if we're happier with some of the fixes!
If you've filed an issue please help me verify and close.
Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!
Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!
As you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,
please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)
---
## ♫ Sounds Behind The Release ♭
* [Glory Box - Portishead](https://www.youtube.com/watch?v=4qQyUi4zfDs)
* [Hit Me With Your Rhythm Stick - Ian Dury And The BlockHeads](https://www.youtube.com/watch?v=0WGVgfjnLqc)
* [Cupidon s'en fout! - George Brassens](https://www.youtube.com/watch?v=a-RlZLfIeKM)
* [Shipbuilding - Elvis Costello](https://www.youtube.com/watch?v=dVhjRqBM5uw)
* [Low Sun - Hermanos Gutierrez](https://www.youtube.com/watch?v=ubaJbw7hkeQ)
---
## A Word From Our Sponsors...
To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!
* [Panfactum](https://github.com/Panfactum)
* [Bastian Pätzold](https://github.com/bastianpaetzold)
* [Mikita Vazhnik](https://github.com/Vazhnik)
* [Jacob Salway](https://github.com/jacobsalway)
* [Eckard Mühlich](https://github.com/eckardnet)
* [Luke](https://github.com/lukepatrick)
* [tomasbanet](https://github.com/tomasbanet)
* [Robin Opletal](https://github.com/fourstepper)
* [Euroblaze](https://github.com/euroblaze)
* [Jack Daniels](https://github.com/dkr91)
* [decafcode](https://github.com/decafcode)
* [Guillaume Copin](https://github.com/GuillaumeCo)
* [Lokalise](https://github.com/lokalise)
* [Gustavo Bini](https://github.com/gustavobini)
* [JMSwag](https://github.com/JMSwag)
* [Daniel Gospodinow](https://github.com/danielgospodinow)
* [Klaviyo](https://github.com/klaviyo)
* [Paul Farver](https://github.com/PaulFarver)
> Sponsorship cancellations since the last release: **12!** 🥹
## 🎉 Feature Release code name: Colon Blow! 🎈
We are pretty stocked about this drop (hopefully...) as we've fully enabled custom columns support in K9s!
Historically, one could customize the view for a given resource by adding a definition in `views.yaml`.
From there one could change sort order and re-arrange the standard column layout.
Several folks voiced the need to add a column for a given label/annotation or any other fields available on a resource.
To date, this wasn't possible 😳
So... without further ado, let see what we can now do with `Custom Views` ding dang deal!
It all starts with a few new directives available in `views.yaml`
### A Refresher...
Customize a pod view and ensure age, ns and name appear first and sort by age descending.
> NOTE! You no longer need to list out all columns.
> The remaining columns will be automatically filled from the standard columns.
```yaml
# Usual biz...
views:
v1/pods: # specify the gvr you want to customize aka group/version/resource
sortColumn: AGE:desc # set the default ordering to ascending (asc) or descending (desc)
columns: # tell the view which columns to display and in which order
- AGE # ensure age, ns and name are the first 3 cols and backfill the rest
- NAMESPACE
- NAME
- READY|H # => NEW! Do not display the READY column
- NODE|W # => NEW! Show node column only on wide
- IP|WR # => NEW! Pull the ip column and right align it in wide mode only
```
## Colon Blow!
Say your pods comes standard with a label `blee` and you want to show it while in pod view.
```yaml
# Pull labels/annotations
views:
v3/freds:
sortColumn: NAMESPACE:dsc
columns:
- NAMESPACE
- NAME
- BLEE:.metadata.labels.blee # => NEW! Pull values from a label or an annotation using json parser
# expression similar mechanic as kubectl -o custom-columns
- ZORG:.spec.zips[?(@.type == 'zorg')].ip|WR # => NEW! Same deal with a json exp + but align right and show wide only
```
## TLDR...
As you can see the CustomView feature adds a few new semantics on this drop.
You can now use the following shape for columns definition `COL_NAME<:json_parse_expression><|column attributes>`
The `:json_parse_expression` is optional.
The column attributes are as follows:
* `T` -> time column indicator
* `N` -> number column indicator
* `W` -> turns on wide column aka only shows while in wide mode. Defaults to the standard resource definition when present.
* `H` -> Hides the column
* `L` -> Left align (default)
* `R` -> Right align
When certain columns are not present in the custom view, K9s will pull the standard column definition and merge the columns.
This allows user to specify and order which columns they want to see first without having to define every single columns from the default resource representation. If you do not wish to see all these columns you can add them to your custom view definition and either specify `|W` or `|H` to `wide` it or `hide` it.
> 📢 Still work in progress so your mileage may vary!
> This feature will likely need additional TLC.
> Your feedback on this will be much appreciated and we will iterate as usual to ensure it vorks as prescribed... 🙀
---
## Videos Are In The Can!
Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...
* [K9s v0.40.0 Colon Blow Sneak peek](https://youtu.be/iy6RDozAM4A)
* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)
* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)
* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)
---
## Resolved Issues
* [#3064](https://github.com/derailed/k9s/issues/3064) Question: brew formula k9s vs derailed/k9s/k9s
* [#3061](https://github.com/derailed/k9s/issues/3061) k9s not opening active namespace or namespace specified via -n
* [#3044](https://github.com/derailed/k9s/issues/3044) CRDs are loaded incorrectly into metadata registry, cause sporadic "Jump Owner" issues
* [#2995](https://github.com/derailed/k9s/issues/2995) Latest image on quay.io contains "failed" kubectl binary
---
## Contributed PRs
Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!
* [#3065](https://github.com/derailed/k9s/pull/3065) Fixed trimming of favorite namespaces in Config
* [#3063](https://github.com/derailed/k9s/pull/3063) Updating CVE dependencies
* [#3062](https://github.com/derailed/k9s/pull/3062) feat: use kubectl events for plugin watch-events
* [#3060](https://github.com/derailed/k9s/pull/3060) Rename "delete local data" checkbox description in drain dialog
* [#3046](https://github.com/derailed/k9s/pull/3046) Strict unmarshal for plugin files
* [#3045](https://github.com/derailed/k9s/pull/3045) fix: CRD loading: trim group suffix from CRD name
* [#3043](https://github.com/derailed/k9s/pull/3043) Fix K9S_EDITOR
* [#3041](https://github.com/derailed/k9s/pull/3041) Fix Flux trace plugin command
* [#3038](https://github.com/derailed/k9s/pull/2038) fix check e != nil but return a nil value error err
* [#3026](https://github.com/derailed/k9s/pull/3026) Fix typos
* [#3018](https://github.com/derailed/k9s/pull/3018) fix: coloring of rose-pine for values of log options
* [#3017](https://github.com/derailed/k9s/pull/3017) feat: add helm diff plugin
* [#3009](https://github.com/derailed/k9s/pull/3009) fix(argo-rollouts plugin): resolve improper piping in watch command
* [#2996](https://github.com/derailed/k9s/pull/2996) Bump version of netshoot image in debug-container plugin
* [#2994](https://github.com/derailed/k9s/pull/2994) fix kubectl url and fail build on download errors
* [#2986](https://github.com/derailed/k9s/pull/2986) plugin/trace-dns: Trace DNS requests using Inspektor Gadget
* [#2985](https://github.com/derailed/k9s/pull/2985) feat(plugins/crossplane): change to crossplane cli & add crossplane-watch
* [#2986](https://github.com/derailed/k9s/pull/2986) plugin/trace-dns: Trace DNS requests using Inspektor Gadget
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -45,16 +45,6 @@ func defaultFGNodeShell() bool {
return false return false
} }
// InList check if string is in a collection of strings.
func InList(ll []string, n string) bool {
for _, l := range ll {
if l == n {
return true
}
}
return false
}
// EnsureDirPath ensures a directory exist from the given path. // EnsureDirPath ensures a directory exist from the given path.
func EnsureDirPath(path string, mod os.FileMode) error { func EnsureDirPath(path string, mod os.FileMode) error {
return EnsureFullPath(filepath.Dir(path), mod) return EnsureFullPath(filepath.Dir(path), mod)

View File

@ -6,6 +6,7 @@ package data_test
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"slices"
"testing" "testing"
"github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/data"
@ -57,7 +58,7 @@ func TestHelperInList(t *testing.T) {
} }
for _, u := range uu { for _, u := range uu {
assert.Equal(t, u.expected, data.InList(u.list, u.item)) assert.Equal(t, u.expected, slices.Contains(u.list, u.item))
} }
} }

View File

@ -4,6 +4,7 @@
package data package data
import ( import (
"slices"
"sync" "sync"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
@ -47,7 +48,7 @@ func (n *Namespace) merge(old *Namespace) {
return return
} }
for _, fav := range old.Favorites { for _, fav := range old.Favorites {
if InList(n.Favorites, fav) { if slices.Contains(n.Favorites, fav) {
continue continue
} }
n.Favorites = append(n.Favorites, fav) n.Favorites = append(n.Favorites, fav)
@ -100,7 +101,7 @@ func (n *Namespace) isAllNamespaces() bool {
} }
func (n *Namespace) addFavNS(ns string) { func (n *Namespace) addFavNS(ns string) {
if InList(n.Favorites, ns) { if slices.Contains(n.Favorites, ns) {
return return
} }

View File

@ -8,9 +8,7 @@ import (
"os/user" "os/user"
"path/filepath" "path/filepath"
"github.com/derailed/k9s/internal/config/data"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
) )
// IsBoolSet checks if a bool ptr is set. // IsBoolSet checks if a bool ptr is set.
@ -44,17 +42,6 @@ func UserTmpDir() (string, error) {
return dir, nil return dir, nil
} }
// InNSList check if ns is in an ns collection.
func InNSList(nn []interface{}, ns string) bool {
ss := make([]string, len(nn))
for i, n := range nn {
if nsp, ok := n.(v1.Namespace); ok {
ss[i] = nsp.Name
}
}
return data.InList(ss, ns)
}
// MustK9sUser establishes current user identity or fail. // MustK9sUser establishes current user identity or fail.
func MustK9sUser() string { func MustK9sUser() string {
usr, err := user.Current() usr, err := user.Current()

View File

@ -1,40 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config_test
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestHelperInNSList(t *testing.T) {
uu := []struct {
item string
list []interface{}
expected bool
}{
{
"fred",
[]interface{}{
v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "fred"}},
},
true,
},
{
"blee",
[]interface{}{
v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "fred"}},
},
false,
},
}
for _, u := range uu {
assert.Equal(t, u.expected, config.InNSList(u.list, u.item))
}
}

View File

@ -14,14 +14,14 @@ import (
"github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/json" "github.com/derailed/k9s/internal/config/json"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// ViewConfigListener represents a view config listener. // ViewConfigListener represents a view config listener.
type ViewConfigListener interface { type ViewConfigListener interface {
// ViewSettingsChanged notifies listener the view configuration changed. // ViewSettingsChanged notifies listener the view configuration changed.
ViewSettingsChanged(ViewSetting) ViewSettingsChanged(*ViewSetting)
} }
// ViewSetting represents a view configuration. // ViewSetting represents a view configuration.
@ -35,7 +35,7 @@ func (v *ViewSetting) HasCols() bool {
} }
func (v *ViewSetting) IsBlank() bool { func (v *ViewSetting) IsBlank() bool {
return v == nil || len(v.Columns) == 0 return v == nil || (len(v.Columns) == 0 && v.SortColumn == "")
} }
func (v *ViewSetting) SortCol() (string, bool, error) { func (v *ViewSetting) SortCol() (string, bool, error) {
@ -52,7 +52,7 @@ func (v *ViewSetting) SortCol() (string, bool, error) {
func (v *ViewSetting) Equals(vs *ViewSetting) bool { func (v *ViewSetting) Equals(vs *ViewSetting) bool {
if v == nil || vs == nil { if v == nil || vs == nil {
return v == nil && vs == nil return false
} }
if c := slices.Compare(v.Columns, vs.Columns); c != 0 { if c := slices.Compare(v.Columns, vs.Columns); c != 0 {
return false return false
@ -116,10 +116,11 @@ func (v *CustomView) RemoveListener(gvr string) {
func (v *CustomView) fireConfigChanged() { func (v *CustomView) fireConfigChanged() {
for gvr, list := range v.listeners { for gvr, list := range v.listeners {
if view, ok := v.Views[gvr]; ok { if vs, ok := v.Views[gvr]; ok {
list.ViewSettingsChanged(view) log.Debug().Msgf("Reloading custom view settings for %s", gvr)
list.ViewSettingsChanged(&vs)
} else { } else {
list.ViewSettingsChanged(ViewSetting{}) list.ViewSettingsChanged(nil)
} }
} }
} }

View File

@ -23,7 +23,7 @@ func TestViewSetting_Equals(t *testing.T) {
v1, v2 *config.ViewSetting v1, v2 *config.ViewSetting
equals bool equals bool
}{ }{
{nil, nil, true}, {nil, nil, false},
{&config.ViewSetting{}, nil, false}, {&config.ViewSetting{}, nil, false},
{nil, &config.ViewSetting{}, false}, {nil, &config.ViewSetting{}, false},
{&config.ViewSetting{}, &config.ViewSetting{}, true}, {&config.ViewSetting{}, &config.ViewSetting{}, true},

145
internal/dao/dynamic.go Normal file
View File

@ -0,0 +1,145 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package dao
import (
"context"
"errors"
"fmt"
"strings"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/rest"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
type Dynamic struct {
Generic
}
func (d *Dynamic) Get(ctx context.Context, path string) (runtime.Object, error) {
return nil, errors.New("Not implemented")
}
func (d *Dynamic) List(ctx context.Context, ns string) ([]runtime.Object, error) {
strLabel, _ := ctx.Value(internal.KeyLabels).(string)
allNS := client.IsAllNamespaces(ns)
flags := cmdutil.NewMatchVersionFlags(d.getFactory().Client().Config().Flags())
f := cmdutil.NewFactory(flags)
b := f.NewBuilder().
Unstructured().
NamespaceParam(ns).DefaultNamespace().AllNamespaces(allNS).
LabelSelectorParam(strLabel).
FieldSelectorParam("").
RequestChunksOf(0).
ResourceTypeOrNameArgs(true, d.gvr.R()).
ContinueOnError().
Latest().
Flatten().
TransformRequests(d.transformRequests).
Do()
if err := b.Err(); err != nil {
return nil, err
}
infos, err := b.Infos()
if err != nil {
return nil, err
}
oo := make([]runtime.Object, 0, len(infos))
for _, info := range infos {
o, err := decodeIntoTable(info.Object, allNS)
if err != nil {
return nil, err
}
oo = append(oo, o.(*metav1.Table))
}
return oo, nil
}
var recognizedTableVersions = map[schema.GroupVersionKind]bool{
metav1beta1.SchemeGroupVersion.WithKind("Table"): true,
metav1.SchemeGroupVersion.WithKind("Table"): true,
}
func decodeIntoTable(obj runtime.Object, allNs bool) (runtime.Object, error) {
event, isEvent := obj.(*metav1.WatchEvent)
if isEvent {
obj = event.Object.Object
}
if !recognizedTableVersions[obj.GetObjectKind().GroupVersionKind()] {
return nil, fmt.Errorf("attempt to decode non-Table object: %v", obj.GetObjectKind().GroupVersionKind())
}
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return nil, fmt.Errorf("attempt to decode non-Unstructured object")
}
var table metav1.Table
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &table); err != nil {
return nil, err
}
if allNs {
defs := make([]metav1.TableColumnDefinition, 0, len(table.ColumnDefinitions)+1)
defs = append(defs, metav1.TableColumnDefinition{Name: "Namespace", Type: "string"})
defs = append(defs, table.ColumnDefinitions...)
table.ColumnDefinitions = defs
}
for i := range table.Rows {
row := &table.Rows[i]
if row.Object.Raw == nil || row.Object.Object != nil {
continue
}
converted, err := runtime.Decode(unstructured.UnstructuredJSONScheme, row.Object.Raw)
if err != nil {
return nil, err
}
row.Object.Object = converted
var m metav1.Object
if obj := row.Object.Object; obj != nil {
m, _ = meta.Accessor(obj)
}
var ns string
if m != nil {
ns = m.GetNamespace()
}
if allNs {
cells := make([]interface{}, 0, len(row.Cells)+1)
cells = append(cells, ns)
cells = append(cells, row.Cells...)
row.Cells = cells
}
}
if isEvent {
event.Object.Object = &table
return event, nil
}
return &table, nil
}
func (d *Dynamic) transformRequests(req *rest.Request) {
req.SetHeader("Accept", strings.Join([]string{
fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName),
fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName),
"application/json",
}, ","))
if d.includeObj {
req.Param("includeObject", "Object")
}
}

View File

@ -16,8 +16,9 @@ import (
type NonResource struct { type NonResource struct {
Factory Factory
gvr client.GVR gvr client.GVR
mx sync.RWMutex mx sync.RWMutex
includeObj bool
} }
// Init initializes the resource. // Init initializes the resource.
@ -29,6 +30,10 @@ func (n *NonResource) Init(f Factory, gvr client.GVR) {
n.mx.Unlock() n.mx.Unlock()
} }
func (n *NonResource) SetIncludeObject(f bool) {
n.includeObj = f
}
func (n *NonResource) gvrStr() string { func (n *NonResource) gvrStr() string {
n.mx.RLock() n.mx.RLock()
defer n.mx.RUnlock() defer n.mx.RUnlock()

View File

@ -89,8 +89,6 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
client.NewGVR("helm"): &HelmChart{}, client.NewGVR("helm"): &HelmChart{},
client.NewGVR("helm-history"): &HelmHistory{}, client.NewGVR("helm-history"): &HelmHistory{},
client.NewGVR("apiextensions.k8s.io/v1/customresourcedefinitions"): &CustomResourceDefinition{}, client.NewGVR("apiextensions.k8s.io/v1/customresourcedefinitions"): &CustomResourceDefinition{},
// !!BOZO!! Popeye
//client.NewGVR("popeye"): &Popeye{},
} }
r, ok := m[gvr] r, ok := m[gvr]
@ -139,16 +137,6 @@ func (m *Meta) GVK2GVR(gv schema.GroupVersion, kind string) (client.GVR, bool, b
return client.NoGVR, false, false return client.NoGVR, false, false
} }
// IsCRD checks if resource represents a CRD
func IsCRD(r metav1.APIResource) bool {
for _, c := range r.Categories {
if c == crdCat {
return true
}
}
return false
}
// MetaFor returns a resource metadata for a given gvr. // MetaFor returns a resource metadata for a given gvr.
func (m *Meta) MetaFor(gvr client.GVR) (metav1.APIResource, error) { func (m *Meta) MetaFor(gvr client.GVR) (metav1.APIResource, error) {
m.mx.RLock() m.mx.RLock()
@ -161,6 +149,16 @@ func (m *Meta) MetaFor(gvr client.GVR) (metav1.APIResource, error) {
return meta, nil return meta, nil
} }
// IsCRD checks if resource represents a CRD
func IsCRD(r metav1.APIResource) bool {
for _, c := range r.Categories {
if c == crdCat {
return true
}
}
return false
}
// IsK8sMeta checks for non resource meta. // IsK8sMeta checks for non resource meta.
func IsK8sMeta(m metav1.APIResource) bool { func IsK8sMeta(m metav1.APIResource) bool {
for _, c := range m.Categories { for _, c := range m.Categories {

View File

@ -79,6 +79,9 @@ type Accessor interface {
// GVR returns a gvr a string. // GVR returns a gvr a string.
GVR() string GVR() string
// SetIncludeObject toggles object inclusion.
SetIncludeObject(bool)
} }
// DrainOptions tracks drain attributes. // DrainOptions tracks drain attributes.

View File

@ -5,14 +5,45 @@ package model
import ( import (
"context" "context"
"fmt"
"regexp" "regexp"
"time" "time"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
"github.com/sahilm/fuzzy" "github.com/sahilm/fuzzy"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
func getMeta(ctx context.Context, gvr client.GVR) (ResourceMeta, error) {
meta := resourceMeta(gvr)
factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return ResourceMeta{}, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
}
meta.DAO.Init(factory, gvr)
return meta, nil
}
func resourceMeta(gvr client.GVR) ResourceMeta {
meta, ok := Registry[gvr.String()]
if !ok {
meta = ResourceMeta{
DAO: new(dao.Dynamic),
Renderer: new(render.Table),
}
}
if meta.DAO == nil {
meta.DAO = &dao.Resource{}
}
return meta
}
// MetaFQN returns a fully qualified resource name. // MetaFQN returns a fully qualified resource name.
func MetaFQN(m metav1.ObjectMeta) string { func MetaFQN(m metav1.ObjectMeta) string {
return FQN(m.Namespace, m.Name) return FQN(m.Namespace, m.Name)

View File

@ -105,7 +105,7 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check,
if !ok { if !ok {
meta = ResourceMeta{ meta = ResourceMeta{
DAO: &dao.Table{}, DAO: &dao.Table{},
Renderer: &render.Generic{}, Renderer: &render.Table{},
} }
} }
if meta.DAO == nil { if meta.DAO == nil {

View File

@ -82,23 +82,14 @@ var Registry = map[string]ResourceMeta{
DAO: &dao.Alias{}, DAO: &dao.Alias{},
Renderer: &render.Alias{}, Renderer: &render.Alias{},
}, },
// !!BOZO!! Popeye
//"popeye": {
// DAO: &dao.Popeye{},
// Renderer: &render.Popeye{},
//},
//"sanitizer": {
// DAO: &dao.Popeye{},
// TreeRenderer: &xray.Section{},
//},
// Core... // Core...
"v1/endpoints": { // "v1/endpoints": {
Renderer: &render.Endpoints{}, // Renderer: &render.Endpoints{},
}, // },
"v1/pods": { "v1/pods": {
DAO: &dao.Pod{}, DAO: &dao.Pod{},
Renderer: &render.Pod{}, Renderer: render.NewPod(),
TreeRenderer: &xray.Pod{}, TreeRenderer: &xray.Pod{},
}, },
"v1/namespaces": { "v1/namespaces": {

View File

@ -13,6 +13,7 @@ import (
backoff "github.com/cenkalti/backoff/v4" backoff "github.com/cenkalti/backoff/v4"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/model1"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -41,6 +42,7 @@ type Table struct {
instance string instance string
labelFilter string labelFilter string
mx sync.RWMutex mx sync.RWMutex
vs *config.ViewSetting
} }
// NewTable returns a new table model. // NewTable returns a new table model.
@ -52,6 +54,20 @@ func NewTable(gvr client.GVR) *Table {
} }
} }
func (t *Table) SetViewSetting(ctx context.Context, vs *config.ViewSetting) {
t.mx.Lock()
{
t.vs = vs
}
t.mx.Unlock()
if ctx != context.Background() {
if err := t.reconcile(ctx); err != nil {
log.Err(err).Msgf("refresh failed for gvr: %s", t.gvr)
}
}
}
// SetLabelFilter sets the labels filter. // SetLabelFilter sets the labels filter.
func (t *Table) SetLabelFilter(f string) { func (t *Table) SetLabelFilter(f string) {
t.mx.Lock() t.mx.Lock()
@ -192,7 +208,11 @@ func (t *Table) updater(ctx context.Context) {
case <-time.After(rate): case <-time.After(rate):
rate = t.refreshRate rate = t.refreshRate
err := backoff.Retry(func() error { err := backoff.Retry(func() error {
return t.refresh(ctx) if err := t.refresh(ctx); err != nil {
log.Err(err).Msgf("refresh failed for gvr: %s", t.gvr)
return err
}
return nil
}, backoff.WithContext(bf, ctx)) }, backoff.WithContext(bf, ctx))
if err != nil { if err != nil {
log.Warn().Err(err).Msgf("reconciler exited") log.Warn().Err(err).Msgf("reconciler exited")
@ -247,6 +267,7 @@ func (t *Table) reconcile(ctx context.Context) error {
err error err error
) )
meta := resourceMeta(t.gvr) meta := resourceMeta(t.gvr)
meta.DAO.SetIncludeObject(true)
ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter)
if t.instance == "" { if t.instance == "" {
oo, err = t.list(ctx, meta.DAO) oo, err = t.list(ctx, meta.DAO)
@ -257,6 +278,8 @@ func (t *Table) reconcile(ctx context.Context) error {
if err != nil { if err != nil {
return err return err
} }
r := meta.Renderer
r.SetViewSetting(t.vs)
return t.data.Reconcile(ctx, meta.Renderer, oo) return t.data.Reconcile(ctx, meta.Renderer, oo)
} }

View File

@ -173,6 +173,8 @@ type accessor struct {
var _ dao.Accessor = (*accessor)(nil) var _ dao.Accessor = (*accessor)(nil)
func (a *accessor) SetIncludeObject(bool) {}
func (a *accessor) List(ctx context.Context, ns string) ([]runtime.Object, error) { func (a *accessor) List(ctx context.Context, ns string) ([]runtime.Object, error) {
return []runtime.Object{&render.PodWithMetrics{Raw: mustLoad("p1")}}, nil return []runtime.Object{&render.PodWithMetrics{Raw: mustLoad("p1")}}, nil
} }

View File

@ -241,7 +241,7 @@ func (t *Tree) resourceMeta() ResourceMeta {
if !ok { if !ok {
meta = ResourceMeta{ meta = ResourceMeta{
DAO: &dao.Table{}, DAO: &dao.Table{},
Renderer: &render.Generic{}, Renderer: &render.Table{},
} }
} }
if meta.DAO == nil { if meta.DAO == nil {

View File

@ -15,7 +15,6 @@ import (
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/sahilm/fuzzy" "github.com/sahilm/fuzzy"
) )
@ -208,29 +207,3 @@ func (y *YAML) ToYAML(ctx context.Context, gvr client.GVR, path string, showMana
return desc.ToYAML(path, showManaged) return desc.ToYAML(path, showManaged)
} }
func getMeta(ctx context.Context, gvr client.GVR) (ResourceMeta, error) {
meta := resourceMeta(gvr)
factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return ResourceMeta{}, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
}
meta.DAO.Init(factory, gvr)
return meta, nil
}
func resourceMeta(gvr client.GVR) ResourceMeta {
meta, ok := Registry[gvr.String()]
if !ok {
meta = ResourceMeta{
DAO: &dao.Table{},
Renderer: &render.Generic{},
}
}
if meta.DAO == nil {
meta.DAO = &dao.Resource{}
}
return meta
}

View File

@ -12,6 +12,9 @@ type DeltaRow []string
func NewDeltaRow(o, n Row, h Header) DeltaRow { func NewDeltaRow(o, n Row, h Header) DeltaRow {
deltas := make(DeltaRow, len(o.Fields)) deltas := make(DeltaRow, len(o.Fields))
for i, old := range o.Fields { for i, old := range o.Fields {
if i >= len(n.Fields) {
continue
}
if old != "" && old != n.Fields[i] && !h.IsTimeCol(i) { if old != "" && old != n.Fields[i] && !h.IsTimeCol(i) {
deltas[i] = old deltas[i] = old
} }

View File

@ -4,6 +4,7 @@
package model1 package model1
import ( import (
"fmt"
"reflect" "reflect"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -11,16 +12,52 @@ import (
const ageCol = "AGE" const ageCol = "AGE"
// HeaderColumn represent a table header. type Attrs struct {
type HeaderColumn struct {
Name string
Align int Align int
Decorator DecoratorFunc Decorator DecoratorFunc
Wide bool Wide bool
MX bool MX bool
MXC, MXM bool
Time bool Time bool
Capacity bool Capacity bool
VS bool VS bool
Hide bool
}
func (a Attrs) Merge(b Attrs) Attrs {
a.MX = b.MX
a.MXC = b.MXC
a.MXM = b.MXM
a.Decorator = b.Decorator
a.VS = b.VS
if a.Align == 0 {
a.Align = b.Align
}
if !a.Wide {
a.Wide = b.Wide
}
if !a.Time {
a.Time = b.Time
}
if !a.Capacity {
a.Capacity = b.Capacity
}
if !a.Hide {
a.Hide = b.Hide
}
return a
}
// HeaderColumn represent a table header.
type HeaderColumn struct {
Attrs
Name string
}
func (h HeaderColumn) String() string {
return fmt.Sprintf("%s [%d::%t::%t::%t]", h.Name, h.Align, h.Wide, h.MX, h.Time)
} }
// Clone copies a header. // Clone copies a header.
@ -106,7 +143,6 @@ func (h Header) Customize(cols []string, wide bool) Header {
col.Wide = false col.Wide = false
cc = append(cc, col) cc = append(cc, col)
} }
if !wide { if !wide {
return cc return cc
} }

View File

@ -104,7 +104,7 @@ func TestHeaderCustomize(t *testing.T) {
"reverse": { "reverse": {
h: model1.Header{ h: model1.Header{
model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "B", Wide: true}, model1.HeaderColumn{Name: "B", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
}, },
cols: []string{"C", "A"}, cols: []string{"C", "A"},
@ -116,7 +116,7 @@ func TestHeaderCustomize(t *testing.T) {
"reverse-wide": { "reverse-wide": {
h: model1.Header{ h: model1.Header{
model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "B", Wide: true}, model1.HeaderColumn{Name: "B", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
}, },
cols: []string{"C", "A"}, cols: []string{"C", "A"},
@ -124,27 +124,27 @@ func TestHeaderCustomize(t *testing.T) {
e: model1.Header{ e: model1.Header{
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "B", Wide: true}, model1.HeaderColumn{Name: "B", Attrs: model1.Attrs{Wide: true}},
}, },
}, },
"toggle-wide": { "toggle-wide": {
h: model1.Header{ h: model1.Header{
model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "B", Wide: true}, model1.HeaderColumn{Name: "B", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
}, },
cols: []string{"C", "B"}, cols: []string{"C", "B"},
wide: true, wide: true,
e: model1.Header{ e: model1.Header{
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
model1.HeaderColumn{Name: "B", Wide: false}, model1.HeaderColumn{Name: "B", Attrs: model1.Attrs{Wide: false}},
model1.HeaderColumn{Name: "A", Wide: true}, model1.HeaderColumn{Name: "A", Attrs: model1.Attrs{Wide: true}},
}, },
}, },
"missing": { "missing": {
h: model1.Header{ h: model1.Header{
model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "B", Wide: true}, model1.HeaderColumn{Name: "B", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
}, },
cols: []string{"BLEE", "A"}, cols: []string{"BLEE", "A"},
@ -152,8 +152,8 @@ func TestHeaderCustomize(t *testing.T) {
e: model1.Header{ e: model1.Header{
model1.HeaderColumn{Name: "BLEE"}, model1.HeaderColumn{Name: "BLEE"},
model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "B", Wide: true}, model1.HeaderColumn{Name: "B", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "C", Wide: true}, model1.HeaderColumn{Name: "C", Attrs: model1.Attrs{Wide: true}},
}, },
}, },
} }
@ -183,7 +183,7 @@ func TestHeaderDiff(t *testing.T) {
"differ-wide": { "differ-wide": {
h1: model1.Header{ h1: model1.Header{
model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "B", Wide: true}, model1.HeaderColumn{Name: "B", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
}, },
h2: model1.Header{ h2: model1.Header{
@ -196,13 +196,13 @@ func TestHeaderDiff(t *testing.T) {
"differ-order": { "differ-order": {
h1: model1.Header{ h1: model1.Header{
model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "B", Wide: true}, model1.HeaderColumn{Name: "B", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
}, },
h2: model1.Header{ h2: model1.Header{
model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
model1.HeaderColumn{Name: "B", Wide: true}, model1.HeaderColumn{Name: "B", Attrs: model1.Attrs{Wide: true}},
}, },
e: true, e: true,
}, },
@ -236,8 +236,8 @@ func TestHeaderHasAge(t *testing.T) {
"age": { "age": {
h: model1.Header{ h: model1.Header{
model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "B", Wide: true}, model1.HeaderColumn{Name: "B", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
}, },
e: true, e: true,
age: true, age: true,
@ -312,7 +312,7 @@ func TestHeaderClone(t *testing.T) {
func makeHeader() model1.Header { func makeHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "B", Wide: true}, model1.HeaderColumn{Name: "B", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
} }
} }

View File

@ -322,35 +322,21 @@ func (t *TableData) Labelize(labels []string) *TableData {
} }
// Customize returns a new model with customized column layout. // Customize returns a new model with customized column layout.
func (t *TableData) Customize(vs *config.ViewSetting, sc SortColumn, manual, wide bool) (*TableData, SortColumn) { func (t *TableData) Customize(vs *config.ViewSetting, sc SortColumn, manual bool) (*TableData, SortColumn) {
if vs.IsBlank() { if vs.IsBlank() {
if sc.Name != "" { if sc.Name != "" {
return t, sc return t, sc
} }
psc, err := t.sortCol(vs) if psc, err := t.sortCol(vs); err == nil {
if err == nil {
return t, psc return t, psc
} }
return t, sc return t, sc
} }
if s, asc, err := vs.SortCol(); err == nil {
cols := vs.Columns return t, SortColumn{Name: s, ASC: asc}
cdata := TableData{
gvr: t.gvr,
namespace: t.namespace,
header: t.header.Customize(cols, wide),
}
ids := t.header.MapIndices(cols, wide)
cdata.rowEvents = t.rowEvents.Customize(ids)
if manual || vs == nil {
return &cdata, sc
}
psc, err := cdata.sortCol(vs)
if err != nil {
return &cdata, sc
} }
return &cdata, psc return t, sc
} }
func (t *TableData) sortCol(vs *config.ViewSetting) (SortColumn, error) { func (t *TableData) sortCol(vs *config.ViewSetting) (SortColumn, error) {

View File

@ -57,7 +57,7 @@ func TestTableDataCustomize(t *testing.T) {
client.NewGVR("test"), client.NewGVR("test"),
Header{ Header{
HeaderColumn{Name: "A"}, HeaderColumn{Name: "A"},
HeaderColumn{Name: "B", Wide: true}, HeaderColumn{Name: "B", Attrs: Attrs{Wide: true}},
HeaderColumn{Name: "C"}, HeaderColumn{Name: "C"},
}, },
NewRowEventsWithEvts( NewRowEventsWithEvts(
@ -71,7 +71,7 @@ func TestTableDataCustomize(t *testing.T) {
client.NewGVR("test"), client.NewGVR("test"),
Header{ Header{
HeaderColumn{Name: "A"}, HeaderColumn{Name: "A"},
HeaderColumn{Name: "B", Wide: false}, HeaderColumn{Name: "B", Attrs: Attrs{Wide: true}},
HeaderColumn{Name: "C"}, HeaderColumn{Name: "C"},
}, },
NewRowEventsWithEvts( NewRowEventsWithEvts(
@ -86,7 +86,7 @@ func TestTableDataCustomize(t *testing.T) {
client.NewGVR("test"), client.NewGVR("test"),
Header{ Header{
HeaderColumn{Name: "A"}, HeaderColumn{Name: "A"},
HeaderColumn{Name: "B", Wide: true}, HeaderColumn{Name: "B", Attrs: Attrs{Wide: true}},
HeaderColumn{Name: "C"}, HeaderColumn{Name: "C"},
}, },
NewRowEventsWithEvts( NewRowEventsWithEvts(
@ -101,13 +101,13 @@ func TestTableDataCustomize(t *testing.T) {
client.NewGVR("test"), client.NewGVR("test"),
Header{ Header{
HeaderColumn{Name: "A"}, HeaderColumn{Name: "A"},
HeaderColumn{Name: "B", Attrs: Attrs{Wide: true}},
HeaderColumn{Name: "C"}, HeaderColumn{Name: "C"},
HeaderColumn{Name: "B", Wide: true},
}, },
NewRowEventsWithEvts( NewRowEventsWithEvts(
RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "3", "2"}}}, RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "3", "2"}}}, RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "3", "2"}}}, RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
), ),
), ),
}, },
@ -116,7 +116,7 @@ func TestTableDataCustomize(t *testing.T) {
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
td, _ := u.t1.Customize(&u.vs, u.sc, u.manual, u.wide) td, _ := u.t1.Customize(&u.vs, u.sc, u.manual)
assert.Equal(t, u.e, td) assert.Equal(t, u.e, td)
}) })
} }

View File

@ -4,6 +4,7 @@
package model1 package model1
import ( import (
"github.com/derailed/k9s/internal/config"
"github.com/derailed/tcell/v2" "github.com/derailed/tcell/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@ -46,6 +47,8 @@ type Renderer interface {
// ColorerFunc returns a row colorer function. // ColorerFunc returns a row colorer function.
ColorerFunc() ColorerFunc ColorerFunc() ColorerFunc
SetViewSetting(vs *config.ViewSetting)
} }
// Generic represents a generic resource. // Generic represents a generic resource.

View File

@ -4,7 +4,9 @@
package render package render
import ( import (
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/model1"
"github.com/rs/zerolog/log"
) )
// DecoratorFunc decorates a string. // DecoratorFunc decorates a string.
@ -15,19 +17,49 @@ var AgeDecorator = func(a string) string {
return toAgeHuman(a) return toAgeHuman(a)
} }
type Base struct{} type Base struct {
vs *config.ViewSetting
specs ColumnSpecs
includeObj bool
}
func (b *Base) SetIncludeObject(f bool) {
b.includeObj = f
}
// IsGeneric identifies a generic handler. // IsGeneric identifies a generic handler.
func (Base) IsGeneric() bool { func (*Base) IsGeneric() bool {
return false return false
} }
func (b *Base) doHeader(dh model1.Header) model1.Header {
if b.specs.isEmpty() {
return dh
}
return b.specs.Header(dh)
}
func (b *Base) SetViewSetting(vs *config.ViewSetting) {
var cols []string
b.vs = vs
if vs != nil {
cols = vs.Columns
}
specs, err := NewColsSpecs(cols...).parseSpecs()
if err != nil {
log.Error().Err(err).Msg("unable to grok custom columns")
return
}
b.specs = specs
}
// ColorerFunc colors a resource row. // ColorerFunc colors a resource row.
func (Base) ColorerFunc() model1.ColorerFunc { func (*Base) ColorerFunc() model1.ColorerFunc {
return model1.DefaultColorer return model1.DefaultColorer
} }
// Happy returns true if resource is happy, false otherwise. // Happy returns true if resource is happy, false otherwise.
func (Base) Happy(string, *model1.Row) bool { func (*Base) Happy(string, *model1.Row) bool {
return true return true
} }

View File

@ -51,12 +51,12 @@ func (Benchmark) Header(ns string) model1.Header {
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "STATUS"}, model1.HeaderColumn{Name: "STATUS"},
model1.HeaderColumn{Name: "TIME"}, model1.HeaderColumn{Name: "TIME"},
model1.HeaderColumn{Name: "REQ/S", Align: tview.AlignRight}, model1.HeaderColumn{Name: "REQ/S", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "2XX", Align: tview.AlignRight}, model1.HeaderColumn{Name: "2XX", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "4XX/5XX", Align: tview.AlignRight}, model1.HeaderColumn{Name: "4XX/5XX", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "REPORT"}, model1.HeaderColumn{Name: "REPORT"},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }

View File

@ -7,12 +7,11 @@ import (
"fmt" "fmt"
"strconv" "strconv"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/model1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
) )
// ConfigMap renders a K8s ConfigMap to screen. // ConfigMap renders a K8s ConfigMap to screen.
@ -20,19 +19,42 @@ type ConfigMap struct {
Base Base
} }
// Header returns a header row.
func (m ConfigMap) Header(_ string) model1.Header {
return m.doHeader(m.defaultHeader())
}
// Header returns a header rbw. // Header returns a header rbw.
func (ConfigMap) Header(string) model1.Header { func (ConfigMap) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "DATA"}, model1.HeaderColumn{Name: "DATA"},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (n ConfigMap) Render(o interface{}, _ string, r *model1.Row) error { func (m ConfigMap) Render(o interface{}, ns string, row *model1.Row) error {
if err := m.defaultRow(o, row); err != nil {
return err
}
if m.specs.isEmpty() {
return nil
}
cols, err := m.specs.realize(o.(*unstructured.Unstructured), m.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
// Render renders a K8s resource to screen.
func (ConfigMap) defaultRow(o interface{}, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected ConfigMap, but got %T", o) return fmt.Errorf("expected ConfigMap, but got %T", o)

View File

@ -70,7 +70,12 @@ func (c Container) ColorerFunc() model1.ColorerFunc {
} }
// Header returns a header row. // Header returns a header row.
func (Container) Header(ns string) model1.Header { func (c Container) Header(_ string) model1.Header {
return c.defaultHeader()
}
// Header returns a header row.
func (Container) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "IDX"}, model1.HeaderColumn{Name: "IDX"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
@ -78,45 +83,49 @@ func (Container) Header(ns string) model1.Header {
model1.HeaderColumn{Name: "IMAGE"}, model1.HeaderColumn{Name: "IMAGE"},
model1.HeaderColumn{Name: "READY"}, model1.HeaderColumn{Name: "READY"},
model1.HeaderColumn{Name: "STATE"}, model1.HeaderColumn{Name: "STATE"},
model1.HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, model1.HeaderColumn{Name: "RESTARTS", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "PROBES(L:R:S)"}, model1.HeaderColumn{Name: "PROBES(L:R:S)"},
model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "CPU", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "MEM", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight}, model1.HeaderColumn{Name: "CPU/R:L", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight}, model1.HeaderColumn{Name: "MEM/R:L", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "%CPU/R", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "%CPU/L", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "%MEM/R", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "%MEM/L", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "PORTS"}, model1.HeaderColumn{Name: "PORTS"},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (c Container) Render(o interface{}, name string, r *model1.Row) error { func (c Container) Render(o interface{}, ns string, row *model1.Row) error {
co, ok := o.(ContainerRes) cr, ok := o.(ContainerRes)
if !ok { if !ok {
return fmt.Errorf("expected ContainerRes, but got %T", o) return fmt.Errorf("expected ContainerRes, but got %T", o)
} }
cur, res := gatherMetrics(co.Container, co.MX) return c.defaultRow(cr, row)
}
func (c Container) defaultRow(cr ContainerRes, r *model1.Row) error {
cur, res := gatherMetrics(cr.Container, cr.MX)
ready, state, restarts := "false", MissingValue, "0" ready, state, restarts := "false", MissingValue, "0"
if co.Status != nil { if cr.Status != nil {
ready, state, restarts = boolToStr(co.Status.Ready), ToContainerState(co.Status.State), strconv.Itoa(int(co.Status.RestartCount)) ready, state, restarts = boolToStr(cr.Status.Ready), ToContainerState(cr.Status.State), strconv.Itoa(int(cr.Status.RestartCount))
} }
r.ID = co.Container.Name r.ID = cr.Container.Name
r.Fields = model1.Fields{ r.Fields = model1.Fields{
co.Idx, cr.Idx,
co.Container.Name, cr.Container.Name,
"●", "●",
co.Container.Image, cr.Container.Image,
ready, ready,
state, state,
restarts, restarts,
probe(co.Container.LivenessProbe) + ":" + probe(co.Container.ReadinessProbe) + ":" + probe(co.Container.StartupProbe), probe(cr.Container.LivenessProbe) + ":" + probe(cr.Container.ReadinessProbe) + ":" + probe(cr.Container.StartupProbe),
toMc(cur.cpu), toMc(cur.cpu),
toMi(cur.mem), toMi(cur.mem),
toMc(res.cpu) + ":" + toMc(res.lcpu), toMc(res.cpu) + ":" + toMc(res.lcpu),
@ -125,9 +134,9 @@ func (c Container) Render(o interface{}, name string, r *model1.Row) error {
client.ToPercentageStr(cur.cpu, res.lcpu), client.ToPercentageStr(cur.cpu, res.lcpu),
client.ToPercentageStr(cur.mem, res.mem), client.ToPercentageStr(cur.mem, res.mem),
client.ToPercentageStr(cur.mem, res.lmem), client.ToPercentageStr(cur.mem, res.lmem),
ToContainerPorts(co.Container.Ports), ToContainerPorts(cr.Container.Ports),
AsStatus(c.diagnose(state, ready)), AsStatus(c.diagnose(state, ready)),
ToAge(co.Age), ToAge(cr.Age),
} }
return nil return nil

View File

@ -18,21 +18,44 @@ type ClusterRole struct {
Base Base
} }
// Header returns a header row.
func (c ClusterRole) Header(_ string) model1.Header {
return c.doHeader(c.defaultHeader())
}
// Header returns a header rbw. // Header returns a header rbw.
func (ClusterRole) Header(string) model1.Header { func (ClusterRole) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (ClusterRole) Render(o interface{}, ns string, r *model1.Row) error { func (p ClusterRole) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expecting clusterrole, but got %T", o) return fmt.Errorf("expecting clusterrole, but got %T", o)
} }
if err := p.defaultRow(raw, row); err != nil {
return err
}
if p.specs.isEmpty() {
return nil
}
cols, err := p.specs.realize(raw, p.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
// Render renders a K8s resource to screen.
func (ClusterRole) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var cr rbacv1.ClusterRole var cr rbacv1.ClusterRole
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cr) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cr)
if err != nil { if err != nil {

View File

@ -18,24 +18,47 @@ type ClusterRoleBinding struct {
Base Base
} }
// Header returns a header row.
func (c ClusterRoleBinding) Header(_ string) model1.Header {
return c.doHeader(c.defaultHeader())
}
// Header returns a header rbw. // Header returns a header rbw.
func (ClusterRoleBinding) Header(string) model1.Header { func (ClusterRoleBinding) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "CLUSTERROLE"}, model1.HeaderColumn{Name: "CLUSTERROLE"},
model1.HeaderColumn{Name: "SUBJECT-KIND"}, model1.HeaderColumn{Name: "SUBJECT-KIND"},
model1.HeaderColumn{Name: "SUBJECTS"}, model1.HeaderColumn{Name: "SUBJECTS"},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (ClusterRoleBinding) Render(o interface{}, ns string, r *model1.Row) error { func (c ClusterRoleBinding) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected ClusterRoleBinding, but got %T", o) return fmt.Errorf("expected ClusterRoleBinding, but got %T", o)
} }
if err := c.defaultRow(raw, row); err != nil {
return err
}
if c.specs.isEmpty() {
return nil
}
// !BOZO!! Call header 2 times
cols, err := c.specs.realize(raw, c.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (ClusterRoleBinding) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var crb rbacv1.ClusterRoleBinding var crb rbacv1.ClusterRoleBinding
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &crb) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &crb)
if err != nil { if err != nil {

View File

@ -21,28 +21,52 @@ type CustomResourceDefinition struct {
Base Base
} }
// Header returns a header row.
func (c CustomResourceDefinition) Header(_ string) model1.Header {
return c.doHeader(c.defaultHeader())
}
// Header returns a header rbw. // Header returns a header rbw.
func (CustomResourceDefinition) Header(string) model1.Header { func (CustomResourceDefinition) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "GROUP"}, model1.HeaderColumn{Name: "GROUP"},
model1.HeaderColumn{Name: "KIND"}, model1.HeaderColumn{Name: "KIND"},
model1.HeaderColumn{Name: "VERSIONS"}, model1.HeaderColumn{Name: "VERSIONS"},
model1.HeaderColumn{Name: "SCOPE"}, model1.HeaderColumn{Name: "SCOPE"},
model1.HeaderColumn{Name: "ALIASES", Wide: true}, model1.HeaderColumn{Name: "ALIASES", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (c CustomResourceDefinition) Render(o interface{}, ns string, r *model1.Row) error { func (c CustomResourceDefinition) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected CustomResourceDefinition, but got %T", o) return fmt.Errorf("expected CustomResourceDefinition, but got %T", o)
} }
if err := c.defaultRow(raw, row); err != nil {
return err
}
if c.specs.isEmpty() {
return nil
}
// !BOZO!! Call header 2 times
cols, err := c.specs.realize(raw, c.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
// Render renders a K8s resource to screen.
func (c CustomResourceDefinition) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var crd v1.CustomResourceDefinition var crd v1.CustomResourceDefinition
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &crd) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &crd)
if err != nil { if err != nil {
@ -114,19 +138,3 @@ func (c CustomResourceDefinition) diagnose(n string, vv []v1.CustomResourceDefin
return errors.New(strings.Join(errs, " - ")) return errors.New(strings.Join(errs, " - "))
} }
func extractMetaField(m map[string]interface{}, field string) string {
f, ok := m[field]
if !ok {
log.Error().Err(fmt.Errorf("failed to extract field from meta %s", field))
return NAValue
}
fs, ok := f.(string)
if !ok {
log.Error().Err(fmt.Errorf("failed to extract string from field %s", field))
return NAValue
}
return fs
}

View File

@ -22,30 +22,52 @@ type CronJob struct {
} }
// Header returns a header row. // Header returns a header row.
func (CronJob) Header(ns string) model1.Header { func (c CronJob) Header(_ string) model1.Header {
return c.doHeader(c.defaultHeader())
}
func (CronJob) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "VS", VS: true}, model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}},
model1.HeaderColumn{Name: "SCHEDULE"}, model1.HeaderColumn{Name: "SCHEDULE"},
model1.HeaderColumn{Name: "SUSPEND"}, model1.HeaderColumn{Name: "SUSPEND"},
model1.HeaderColumn{Name: "ACTIVE"}, model1.HeaderColumn{Name: "ACTIVE"},
model1.HeaderColumn{Name: "LAST_SCHEDULE", Time: true}, model1.HeaderColumn{Name: "LAST_SCHEDULE", Attrs: model1.Attrs{Time: true}},
model1.HeaderColumn{Name: "SELECTOR", Wide: true}, model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "CONTAINERS", Wide: true}, model1.HeaderColumn{Name: "CONTAINERS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "IMAGES", Wide: true}, model1.HeaderColumn{Name: "IMAGES", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (c CronJob) Render(o interface{}, ns string, r *model1.Row) error { func (c CronJob) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected CronJob, but got %T", o) return fmt.Errorf("expected CronJob, but got %T", o)
} }
if err := c.defaultRow(raw, row); err != nil {
return err
}
if c.specs.isEmpty() {
return nil
}
cols, err := c.specs.realize(raw, c.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
// Render renders a K8s resource to screen.
func (c CronJob) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var cj batchv1.CronJob var cj batchv1.CronJob
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cj) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cj)
if err != nil { if err != nil {

104
internal/render/cust_col.go Normal file
View File

@ -0,0 +1,104 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package render
import (
"fmt"
"regexp"
"github.com/derailed/k9s/internal/model1"
"github.com/derailed/tview"
"k8s.io/kubectl/pkg/cmd/get"
)
var fullRX = regexp.MustCompile(`\A([\w\s-]+)\:?([^\|]*)\|?([T|N|W|L|R|H]{0,3})\b`)
type colAttr byte
const (
number colAttr = 'N'
age colAttr = 'T'
wide colAttr = 'W'
alignLeft colAttr = 'L'
alignRight colAttr = 'R'
hide colAttr = 'H'
)
type colAttrs struct {
align int
mx bool
mxc bool
mxm bool
time bool
wide bool
hide bool
capacity bool
}
func newColFlags(flags string) colAttrs {
c := colAttrs{
align: tview.AlignLeft,
wide: false,
}
for _, b := range []byte(flags) {
switch colAttr(b) {
case hide:
c.hide = true
case wide:
c.wide = true
case alignLeft:
c.align = tview.AlignLeft
case alignRight:
c.align = tview.AlignRight
case age:
c.time = true
case number:
c.capacity, c.align = true, tview.AlignRight
}
}
return c
}
type colDef struct {
colAttrs
name string
idx int
spec string
}
func parse(s string) (colDef, error) {
mm := fullRX.FindStringSubmatch(s)
if len(mm) == 4 {
spec, err := get.RelaxedJSONPathExpression(mm[2])
if err != nil {
return colDef{idx: -1}, err
}
return colDef{
name: mm[1],
idx: -1,
spec: spec,
colAttrs: newColFlags(mm[3]),
}, nil
}
return colDef{idx: -1}, fmt.Errorf("invalid column definition %q", s)
}
func (c colDef) toHeaderCol() model1.HeaderColumn {
return model1.HeaderColumn{
Name: c.name,
Attrs: model1.Attrs{
Align: c.align,
Wide: c.wide,
Time: c.time,
MX: c.mx,
MXC: c.mxc,
MXM: c.mxm,
Hide: c.hide,
Capacity: c.capacity,
},
}
}

View File

@ -0,0 +1,240 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package render
import (
"errors"
"testing"
"github.com/derailed/tview"
"github.com/stretchr/testify/assert"
)
func TestCustCol_parse(t *testing.T) {
uu := map[string]struct {
s string
err error
e colDef
}{
"empty": {
err: errors.New(`invalid column definition ""`),
},
"plain": {
s: "fred",
e: colDef{
name: "fred",
idx: -1,
colAttrs: colAttrs{
align: tview.AlignLeft,
},
},
},
"plain-wide": {
s: "fred|W",
e: colDef{
name: "fred",
idx: -1,
colAttrs: colAttrs{
align: tview.AlignLeft,
wide: true,
},
},
},
"plain-hide": {
s: "fred|WH",
e: colDef{
name: "fred",
idx: -1,
colAttrs: colAttrs{
align: tview.AlignLeft,
wide: true,
hide: true,
},
},
},
"age": {
s: "AGE|TR",
e: colDef{
name: "AGE",
idx: -1,
colAttrs: colAttrs{
align: tview.AlignRight,
time: true,
},
},
},
"plain-wide-right": {
s: "fred|WR",
e: colDef{
name: "fred",
idx: -1,
colAttrs: colAttrs{
align: tview.AlignRight,
wide: true,
},
},
},
"complex": {
s: "BLEE:.spec.addresses[?(@.type == 'CiliumInternalIP')].ip",
e: colDef{
name: "BLEE",
idx: -1,
spec: "{.spec.addresses[?(@.type == 'CiliumInternalIP')].ip}",
colAttrs: colAttrs{
align: tview.AlignLeft,
},
},
},
"complex-wide": {
s: "BLEE:.spec.addresses[?(@.type == 'CiliumInternalIP')].ip|WR",
e: colDef{
name: "BLEE",
idx: -1,
spec: "{.spec.addresses[?(@.type == 'CiliumInternalIP')].ip}",
colAttrs: colAttrs{
align: tview.AlignRight,
wide: true,
},
},
},
"full-complex-wide": {
s: "BLEE:.spec.addresses[?(@.type == 'CiliumInternalIP')].ip|WR",
e: colDef{
name: "BLEE",
idx: -1,
spec: "{.spec.addresses[?(@.type == 'CiliumInternalIP')].ip}",
colAttrs: colAttrs{
align: tview.AlignRight,
wide: true,
},
},
},
"full-number-wide": {
s: "fred:.metadata.name|NW",
e: colDef{
name: "fred",
idx: -1,
spec: "{.metadata.name}",
colAttrs: colAttrs{
align: tview.AlignRight,
capacity: true,
wide: true,
},
},
},
"full-wide": {
s: "fred:.metadata.name|RW",
e: colDef{
name: "fred",
idx: -1,
spec: "{.metadata.name}",
colAttrs: colAttrs{
align: tview.AlignRight,
wide: true,
},
},
},
"partial-time-no-wide": {
s: "fred:.metadata.name|T",
e: colDef{
name: "fred",
idx: -1,
spec: "{.metadata.name}",
colAttrs: colAttrs{
align: tview.AlignLeft,
time: true,
},
},
},
"partial-no-type-no-wide": {
s: "fred:.metadata.name",
e: colDef{
name: "fred",
idx: -1,
spec: "{.metadata.name}",
colAttrs: colAttrs{
align: tview.AlignLeft,
},
},
},
"partial-no-type-wide": {
s: "fred:.metadata.name|W",
e: colDef{
name: "fred",
idx: -1,
spec: "{.metadata.name}",
colAttrs: colAttrs{
align: tview.AlignLeft,
wide: true,
},
},
},
"toast": {
s: "fred||.metadata.name|W",
e: colDef{
name: "fred",
idx: -1,
colAttrs: colAttrs{
align: tview.AlignLeft,
},
},
},
"toast-no-name": {
s: `:.metadata.name.fred|TW`,
err: errors.New(`invalid column definition ":.metadata.name.fred|TW"`),
},
"spec-column-typed": {
s: `fred:.metadata.name.k8s:fred\.blee|TW`,
e: colDef{
name: "fred",
spec: `{.metadata.name.k8s:fred\.blee}`,
idx: -1,
colAttrs: colAttrs{
align: tview.AlignLeft,
time: true,
wide: true,
},
},
},
"partial-no-spec-no-wide": {
s: "fred|T",
e: colDef{
name: "fred",
idx: -1,
colAttrs: colAttrs{
align: tview.AlignLeft,
time: true,
},
},
},
}
for k, u := range uu {
t.Run(k, func(t *testing.T) {
c, err := parse(u.s)
if err != nil {
assert.Equal(t, u.err, err)
} else {
assert.Equal(t, u.e, c)
}
})
}
}

View File

@ -0,0 +1,224 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package render
import (
"fmt"
"reflect"
"strings"
"time"
"github.com/derailed/k9s/internal/model1"
"github.com/rs/zerolog/log"
"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"
)
type ColsSpecs []string
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
}
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
}
func (rr RenderedCols) HasHeader(n string) bool {
for _, r := range rr {
if r.has(n) {
return true
}
}
return false
}
type RenderedCol struct {
Header model1.HeaderColumn
Value string
}
func (r RenderedCol) has(n string) bool {
return r.Header.Name == n
}
type ColumnSpec struct {
Header model1.HeaderColumn
Spec string
}
type ColumnSpecs []ColumnSpec
func (c ColumnSpecs) isEmpty() bool {
return len(c) == 0
}
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].Attrs.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,
}
log.Warn().Msgf("unable to find column %s", 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
}

View File

@ -0,0 +1,235 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package render
import (
"errors"
"testing"
"github.com/derailed/k9s/internal/model1"
"github.com/derailed/tview"
"github.com/stretchr/testify/assert"
)
func TestParseSpecs(t *testing.T) {
uu := map[string]struct {
cols ColsSpecs
err error
e ColumnSpecs
}{
"empty": {
e: ColumnSpecs{},
},
"plain": {
cols: ColsSpecs{
"a",
"b",
"c",
},
e: ColumnSpecs{
{
Header: model1.HeaderColumn{
Name: "a",
},
},
{
Header: model1.HeaderColumn{
Name: "b",
},
},
{
Header: model1.HeaderColumn{
Name: "c",
},
},
},
},
"with-spec-plain": {
cols: ColsSpecs{
"a",
"b:.metadata.name",
"c",
},
e: ColumnSpecs{
{
Header: model1.HeaderColumn{
Name: "a",
},
},
{
Header: model1.HeaderColumn{
Name: "b",
},
Spec: "{.metadata.name}",
},
{
Header: model1.HeaderColumn{
Name: "c",
},
},
},
},
"with-spec-fq": {
cols: ColsSpecs{
"a",
"b:.metadata.name|NW",
"c",
},
e: ColumnSpecs{
{
Header: model1.HeaderColumn{
Name: "a",
},
},
{
Header: model1.HeaderColumn{
Name: "b",
Attrs: model1.Attrs{
Wide: true,
Capacity: true,
Align: tview.AlignRight,
},
},
Spec: "{.metadata.name}",
},
{
Header: model1.HeaderColumn{
Name: "c",
},
},
},
},
"spec-type-no-wide": {
cols: ColsSpecs{
"a",
"b:.metadata.name|T",
"c",
},
e: ColumnSpecs{
{
Header: model1.HeaderColumn{
Name: "a",
},
},
{
Header: model1.HeaderColumn{
Name: "b",
Attrs: model1.Attrs{
Time: true,
},
},
Spec: "{.metadata.name}",
},
{
Header: model1.HeaderColumn{
Name: "c",
},
},
},
},
"plain-wide": {
cols: ColsSpecs{
"a",
"b|W",
"c",
},
e: ColumnSpecs{
{
Header: model1.HeaderColumn{
Name: "a",
},
},
{
Header: model1.HeaderColumn{
Name: "b",
Attrs: model1.Attrs{Wide: true},
},
},
{
Header: model1.HeaderColumn{
Name: "c",
},
},
},
},
"no-spec-kind-wide": {
cols: ColsSpecs{
"a",
"b|NW",
"c",
},
e: ColumnSpecs{
{
Header: model1.HeaderColumn{
Name: "a",
},
},
{
Header: model1.HeaderColumn{
Name: "b",
Attrs: model1.Attrs{
Align: tview.AlignRight,
Capacity: true,
Wide: true,
},
},
},
{
Header: model1.HeaderColumn{
Name: "c",
},
},
},
},
"toast-spec": {
cols: ColsSpecs{
"a",
"b:{{crap.bozo}}|NW",
"c",
},
err: errors.New(`unexpected path string, expected a 'name1.name2' or '.name1.name2' or '{name1.name2}' or '{.name1.name2}'`),
},
"no-spec": {
cols: ColsSpecs{
"a",
"b|NW",
"c",
},
e: ColumnSpecs{
{
Header: model1.HeaderColumn{
Name: "a",
},
},
{
Header: model1.HeaderColumn{
Name: "b",
Attrs: model1.Attrs{Align: tview.AlignRight, Capacity: true, Wide: true},
},
},
{
Header: model1.HeaderColumn{
Name: "c",
},
},
},
},
}
for k, u := range uu {
t.Run(k, func(t *testing.T) {
cols, err := u.cols.parseSpecs()
assert.Equal(t, u.err, err)
assert.Equal(t, u.e, cols)
})
}
}

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2" "github.com/derailed/tcell/v2"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -28,6 +29,8 @@ func (Dir) ColorerFunc() model1.ColorerFunc {
} }
} }
func (Dir) SetViewSetting(*config.ViewSetting) {}
// Header returns a header row. // Header returns a header row.
func (Dir) Header(ns string) model1.Header { func (Dir) Header(ns string) model1.Header {
return model1.Header{ return model1.Header{

View File

@ -42,27 +42,49 @@ func (d Deployment) ColorerFunc() model1.ColorerFunc {
} }
// Header returns a header row. // Header returns a header row.
func (Deployment) Header(ns string) model1.Header { func (d Deployment) Header(_ string) model1.Header {
return d.doHeader(d.defaultHeader())
}
func (Deployment) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "VS", VS: true}, model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}},
model1.HeaderColumn{Name: "READY", Align: tview.AlignRight}, model1.HeaderColumn{Name: "READY", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight}, model1.HeaderColumn{Name: "UP-TO-DATE", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight}, model1.HeaderColumn{Name: "AVAILABLE", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (d Deployment) Render(o interface{}, ns string, r *model1.Row) error { func (d Deployment) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected Deployment, but got %T", o) return fmt.Errorf("expected Deployment, but got %T", o)
} }
if err := d.defaultRow(raw, row); err != nil {
return err
}
if d.specs.isEmpty() {
return nil
}
// !BOZO!! Call header 2 times
cols, err := d.specs.realize(raw, d.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
// Render renders a K8s resource to screen.
func (d Deployment) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var dp appsv1.Deployment var dp appsv1.Deployment
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &dp) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &dp)
if err != nil { if err != nil {

View File

@ -21,28 +21,52 @@ type DaemonSet struct {
} }
// Header returns a header row. // Header returns a header row.
func (DaemonSet) Header(ns string) model1.Header { func (d DaemonSet) Header(_ string) model1.Header {
return d.doHeader(d.defaultHeader())
}
// Header returns a header row.
func (DaemonSet) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "VS", VS: true}, model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}},
model1.HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, model1.HeaderColumn{Name: "DESIRED", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, model1.HeaderColumn{Name: "CURRENT", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "READY", Align: tview.AlignRight}, model1.HeaderColumn{Name: "READY", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight}, model1.HeaderColumn{Name: "UP-TO-DATE", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight}, model1.HeaderColumn{Name: "AVAILABLE", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (d DaemonSet) Render(o interface{}, ns string, r *model1.Row) error { func (d DaemonSet) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected DaemonSet, but got %T", o) return fmt.Errorf("expected Deployment, but got %T", o)
} }
if err := d.defaultRow(raw, row); err != nil {
return err
}
if d.specs.isEmpty() {
return nil
}
// !BOZO!! Call header 2 times
cols, err := d.specs.realize(raw, d.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
// Render renders a K8s resource to screen.
func (d DaemonSet) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var ds appsv1.DaemonSet var ds appsv1.DaemonSet
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ds) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ds)
if err != nil { if err != nil {

View File

@ -21,17 +21,38 @@ type Endpoints struct {
} }
// Header returns a header row. // Header returns a header row.
func (Endpoints) Header(ns string) model1.Header { func (e Endpoints) Header(_ string) model1.Header {
return e.doHeader(e.defaultHeader())
}
// Header returns a header row.
func (Endpoints) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "ENDPOINTS"}, model1.HeaderColumn{Name: "ENDPOINTS"},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (e Endpoints) Render(o interface{}, ns string, r *model1.Row) error { func (e Endpoints) Render(o interface{}, ns string, row *model1.Row) error {
if err := e.defaultRow(o, ns, row); err != nil {
return err
}
if e.specs.isEmpty() {
return nil
}
cols, err := e.specs.realize(o.(*unstructured.Unstructured), e.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (e Endpoints) defaultRow(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected Endpoints, but got %T", o) return fmt.Errorf("expected Endpoints, but got %T", o)

View File

@ -15,7 +15,7 @@ import (
// Event renders a K8s Event to screen. // Event renders a K8s Event to screen.
type Event struct { type Event struct {
Generic Table
} }
func (*Event) IsGeneric() bool { func (*Event) IsGeneric() bool {
@ -41,13 +41,15 @@ var ageCols = map[string]struct{}{
var wideCols = map[string]struct{}{ var wideCols = map[string]struct{}{
"SUBOBJECT": {}, "SUBOBJECT": {},
"COUNT": {},
"SOURCE": {}, "SOURCE": {},
"FIRST SEEN": {}, "FIRST SEEN": {},
"NAME": {}, "NAME": {},
"MESSAGE": {}, "MESSAGE": {},
} }
func (e *Event) Header(ns string) model1.Header { // Header returns a header row.
func (e *Event) Header(_ string) model1.Header {
if e.table == nil { if e.table == nil {
return model1.Header{} return model1.Header{}
} }
@ -78,9 +80,6 @@ func (e *Event) Render(o interface{}, ns string, r *model1.Row) error {
return err return err
} }
if !ok {
return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0])
}
r.ID = client.FQN(nns, name) r.ID = client.FQN(nns, name)
r.Fields = make(model1.Fields, 0, len(e.Header(ns))) r.Fields = make(model1.Fields, 0, len(e.Header(ns)))
r.Fields = append(r.Fields, nns) r.Fields = append(r.Fields, nns)

View File

@ -4,141 +4,65 @@
package render package render
import ( import (
"encoding/json"
"errors"
"fmt" "fmt"
"strings"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/model1"
"github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
const ageTableCol = "Age" // Generic renders a K8s generic resource to screen.
// Generic renders a generic resource to screen.
type Generic struct { type Generic struct {
Base Base
table *metav1.Table
header model1.Header
ageIndex int
}
func (*Generic) IsGeneric() bool {
return true
}
// SetTable sets the tabular resource.
func (g *Generic) SetTable(ns string, t *metav1.Table) {
g.table = t
g.header = g.Header(ns)
}
// ColorerFunc colors a resource row.
func (*Generic) ColorerFunc() model1.ColorerFunc {
return model1.DefaultColorer
} }
// Header returns a header row. // Header returns a header row.
func (g *Generic) Header(ns string) model1.Header { func (m Generic) Header(_ string) model1.Header {
if g.header != nil { return m.doHeader(m.defaultHeader())
return g.header }
}
if g.table == nil {
return model1.Header{}
}
h := make(model1.Header, 0, len(g.table.ColumnDefinitions))
if !client.IsClusterScoped(ns) {
h = append(h, model1.HeaderColumn{Name: "NAMESPACE"})
}
for i, c := range g.table.ColumnDefinitions {
if c.Name == ageTableCol {
g.ageIndex = i
continue
}
h = append(h, model1.HeaderColumn{Name: strings.ToUpper(c.Name)})
}
if g.ageIndex > 0 {
h = append(h, model1.HeaderColumn{Name: "AGE", Time: true})
}
return h // Header returns a header rbw.
func (Generic) defaultHeader() model1.Header {
return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
}
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (g *Generic) Render(o interface{}, ns string, r *model1.Row) error { func (m Generic) Render(o interface{}, ns string, row *model1.Row) error {
row, ok := o.(metav1.TableRow) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expecting a TableRow but got %T", o) return fmt.Errorf("expected *Unstructured, but got %T", o)
} }
nns, name, err := resourceNS(row.Object.Raw)
if err := m.defaultRow(raw, row); err != nil {
return err
}
if m.specs.isEmpty() {
return nil
}
cols, err := m.specs.realize(o.(*unstructured.Unstructured), m.defaultHeader(), row)
if err != nil { if err != nil {
return err return err
} }
cols.hydrateRow(row)
if !ok {
return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0])
}
r.ID = client.FQN(nns, name)
r.Fields = make(model1.Fields, 0, len(g.Header(ns)))
if !client.IsClusterScoped(ns) {
r.Fields = append(r.Fields, nns)
}
var duration interface{}
for i, c := range row.Cells {
if g.ageIndex > 0 && i == g.ageIndex {
duration = c
continue
}
if c == nil {
r.Fields = append(r.Fields, Blank)
continue
}
r.Fields = append(r.Fields, fmt.Sprintf("%v", c))
}
if d, ok := duration.(string); ok {
r.Fields = append(r.Fields, d)
} else if g.ageIndex > 0 {
log.Warn().Msgf("No Duration detected on age field")
r.Fields = append(r.Fields, NAValue)
}
return nil return nil
} }
// ---------------------------------------------------------------------------- // Render renders a K8s resource to screen.
// Helpers... func (Generic) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
r.ID = client.FQN(raw.GetNamespace(), raw.GetName())
func resourceNS(raw []byte) (string, string, error) { r.Fields = model1.Fields{
var obj map[string]interface{} raw.GetNamespace(),
var ns, name string raw.GetName(),
err := json.Unmarshal(raw, &obj) "",
if err != nil { ToAge(raw.GetCreationTimestamp()),
return ns, name, err
} }
meta, ok := obj["metadata"].(map[string]interface{}) return nil
if !ok {
return ns, name, errors.New("no metadata found on generic resource")
}
ina, ok := meta["name"]
if !ok {
return ns, name, errors.New("unable to extract resource name")
}
name, ok = ina.(string)
if !ok {
return ns, name, fmt.Errorf("expecting name string type but got %T", ns)
}
ins, ok := meta["namespace"]
if !ok {
return client.ClusterScope, name, nil
}
ns, ok = ins.(string)
if !ok {
return ns, name, fmt.Errorf("expecting namespace string type but got %T", ns)
}
return ns, name, nil
} }

View File

@ -8,6 +8,7 @@ import (
"strconv" "strconv"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
@ -24,6 +25,8 @@ func (Chart) IsGeneric() bool {
return false return false
} }
func (Chart) SetViewSetting(*config.ViewSetting) {}
// ColorerFunc colors a resource row. // ColorerFunc colors a resource row.
func (Chart) ColorerFunc() model1.ColorerFunc { func (Chart) ColorerFunc() model1.ColorerFunc {
return model1.DefaultColorer return model1.DefaultColorer
@ -38,8 +41,8 @@ func (Chart) Header(_ string) model1.Header {
model1.HeaderColumn{Name: "STATUS"}, model1.HeaderColumn{Name: "STATUS"},
model1.HeaderColumn{Name: "CHART"}, model1.HeaderColumn{Name: "CHART"},
model1.HeaderColumn{Name: "APP VERSION"}, model1.HeaderColumn{Name: "APP VERSION"},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }

View File

@ -9,6 +9,7 @@ import (
"strconv" "strconv"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
) )
@ -21,6 +22,8 @@ func (History) Healthy(ctx context.Context, o interface{}) error {
return nil return nil
} }
func (History) SetViewSetting(*config.ViewSetting) {}
// IsGeneric identifies a generic handler. // IsGeneric identifies a generic handler.
func (History) IsGeneric() bool { func (History) IsGeneric() bool {
return false return false
@ -39,7 +42,7 @@ func (History) Header(_ string) model1.Header {
model1.HeaderColumn{Name: "CHART"}, model1.HeaderColumn{Name: "CHART"},
model1.HeaderColumn{Name: "APP VERSION"}, model1.HeaderColumn{Name: "APP VERSION"},
model1.HeaderColumn{Name: "DESCRIPTION"}, model1.HeaderColumn{Name: "DESCRIPTION"},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
} }
} }

View File

@ -223,7 +223,7 @@ func mapToStr(m map[string]string) string {
for i, k := range kk { for i, k := range kk {
bb = append(bb, k+"="+m[k]...) bb = append(bb, k+"="+m[k]...)
if i < len(kk)-1 { if i < len(kk)-1 {
bb = append(bb, ' ') bb = append(bb, ',')
} }
} }
@ -292,16 +292,6 @@ func strPtrToStr(s *string) string {
return *s return *s
} }
// // Check if string is in a string list.
// func in(ll []string, s string) bool {
// for _, l := range ll {
// if l == s {
// return true
// }
// }
// return false
// }
// Pad a string up to the given length or truncates if greater than length. // Pad a string up to the given length or truncates if greater than length.
func Pad(s string, width int) string { func Pad(s string, width int) string {
if len(s) == width { if len(s) == width {
@ -314,30 +304,3 @@ func Pad(s string, width int) string {
return s + strings.Repeat(" ", width-len(s)) return s + strings.Repeat(" ", width-len(s))
} }
// // Converts labels string to map.
// func labelize(labels string) map[string]string {
// ll := strings.Split(labels, ",")
// data := make(map[string]string, len(ll))
// for _, l := range ll {
// tokens := strings.Split(l, "=")
// if len(tokens) == 2 {
// data[tokens[0]] = tokens[1]
// }
// }
// return data
// }
// func sortLabels(m map[string]string) (keys, vals []string) {
// for k := range m {
// keys = append(keys, k)
// }
// sort.Strings(keys)
// for _, k := range keys {
// vals = append(vals, m[k])
// }
// return
// }

View File

@ -20,7 +20,7 @@ import (
) )
func TestTableGenericHydrate(t *testing.T) { func TestTableGenericHydrate(t *testing.T) {
raw := raw(t, "p1") raw := load(t, "p1")
tt := metav1beta1.Table{ tt := metav1beta1.Table{
ColumnDefinitions: []metav1beta1.TableColumnDefinition{ ColumnDefinitions: []metav1beta1.TableColumnDefinition{
{Name: "c1"}, {Name: "c1"},
@ -29,21 +29,21 @@ func TestTableGenericHydrate(t *testing.T) {
Rows: []metav1beta1.TableRow{ Rows: []metav1beta1.TableRow{
{ {
Cells: []interface{}{"fred", 10}, Cells: []interface{}{"fred", 10},
Object: runtime.RawExtension{Raw: raw}, Object: runtime.RawExtension{Object: raw},
}, },
{ {
Cells: []interface{}{"blee", 20}, Cells: []interface{}{"blee", 20},
Object: runtime.RawExtension{Raw: raw}, Object: runtime.RawExtension{Object: raw},
}, },
}, },
} }
rr := make([]model1.Row, 2) rr := make([]model1.Row, 2)
var re Generic var re Table
re.SetTable("blee", &tt) re.SetTable("blee", &tt)
assert.Nil(t, model1.GenericHydrate("blee", &tt, rr, &re)) assert.Nil(t, model1.GenericHydrate("blee", &tt, rr, &re))
assert.Equal(t, 2, len(rr)) assert.Equal(t, 2, len(rr))
assert.Equal(t, 3, len(rr[0].Fields)) assert.Equal(t, 2, len(rr[0].Fields))
} }
func TestTableHydrate(t *testing.T) { func TestTableHydrate(t *testing.T) {
@ -52,7 +52,7 @@ func TestTableHydrate(t *testing.T) {
} }
rr := make([]model1.Row, 1) rr := make([]model1.Row, 1)
assert.Nil(t, model1.Hydrate("blee", oo, rr, Pod{})) assert.Nil(t, model1.Hydrate("blee", oo, rr, NewPod()))
assert.Equal(t, 1, len(rr)) assert.Equal(t, 1, len(rr))
assert.Equal(t, 25, len(rr[0].Fields)) assert.Equal(t, 25, len(rr[0].Fields))
} }
@ -313,7 +313,7 @@ func TestMapToStr(t *testing.T) {
i map[string]string i map[string]string
e string e string
}{ }{
{map[string]string{"blee": "duh", "aa": "bb"}, "aa=bb blee=duh"}, {map[string]string{"blee": "duh", "aa": "bb"}, "aa=bb,blee=duh"},
{map[string]string{}, ""}, {map[string]string{}, ""},
} }
for _, u := range uu { for _, u := range uu {
@ -438,9 +438,3 @@ func load(t *testing.T, n string) *unstructured.Unstructured {
assert.Nil(t, err) assert.Nil(t, err)
return &o return &o
} }
func raw(t *testing.T, n string) []byte {
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
assert.Nil(t, err)
return raw
}

View File

@ -13,7 +13,7 @@ import (
// HorizontalPodAutoscaler renders a K8s HorizontalPodAutoscaler to screen. // HorizontalPodAutoscaler renders a K8s HorizontalPodAutoscaler to screen.
type HorizontalPodAutoscaler struct { type HorizontalPodAutoscaler struct {
Generic Table
} }
// ColorerFunc colors a resource row. // ColorerFunc colors a resource row.

View File

@ -18,10 +18,10 @@ func TestHorizontalPodAutoscalerColorer(t *testing.T) {
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "REFERENCE"}, model1.HeaderColumn{Name: "REFERENCE"},
model1.HeaderColumn{Name: "TARGETS%"}, model1.HeaderColumn{Name: "TARGETS%"},
model1.HeaderColumn{Name: "MINPODS", Align: tview.AlignRight}, model1.HeaderColumn{Name: "MINPODS", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "MAXPODS", Align: tview.AlignRight}, model1.HeaderColumn{Name: "MAXPODS", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "REPLICAS", Align: tview.AlignRight}, model1.HeaderColumn{Name: "REPLICAS", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
uu := map[string]struct { uu := map[string]struct {

View File

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/model1"
@ -24,27 +23,48 @@ type Job struct {
} }
// Header returns a header row. // Header returns a header row.
func (Job) Header(ns string) model1.Header { func (j Job) Header(_ string) model1.Header {
return j.doHeader(j.defaultHeader())
}
func (Job) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "VS", VS: true}, model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}},
model1.HeaderColumn{Name: "COMPLETIONS"}, model1.HeaderColumn{Name: "COMPLETIONS"},
model1.HeaderColumn{Name: "DURATION"}, model1.HeaderColumn{Name: "DURATION"},
model1.HeaderColumn{Name: "SELECTOR", Wide: true}, model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "CONTAINERS", Wide: true}, model1.HeaderColumn{Name: "CONTAINERS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "IMAGES", Wide: true}, model1.HeaderColumn{Name: "IMAGES", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (j Job) Render(o interface{}, ns string, r *model1.Row) error { func (j Job) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected Job, but got %T", o) return fmt.Errorf("expected Job, 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, j.defaultHeader(), 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 var job batchv1.Job
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &job) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &job)
if err != nil { if err != nil {
@ -128,17 +148,9 @@ func toCompletion(spec batchv1.JobSpec, status batchv1.JobStatus) (s string) {
} }
func toDuration(status batchv1.JobStatus) string { func toDuration(status batchv1.JobStatus) string {
if status.StartTime == nil { if status.StartTime == nil || status.CompletionTime == nil {
return MissingValue return MissingValue
} }
var d time.Duration return duration.HumanDuration(status.CompletionTime.Sub(status.StartTime.Time))
switch {
case status.CompletionTime == nil:
d = time.Since(status.StartTime.Time)
default:
d = status.CompletionTime.Sub(status.StartTime.Time)
}
return duration.HumanDuration(d)
} }

View File

@ -31,44 +31,61 @@ type Node struct {
} }
// Header returns a header row. // Header returns a header row.
func (Node) Header(string) model1.Header { func (n Node) Header(_ string) model1.Header {
return n.doHeader(n.defaultHeader())
}
func (Node) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "STATUS"}, model1.HeaderColumn{Name: "STATUS"},
model1.HeaderColumn{Name: "ROLE"}, model1.HeaderColumn{Name: "ROLE"},
model1.HeaderColumn{Name: "ARCH", Wide: true}, model1.HeaderColumn{Name: "ARCH", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "TAINTS"}, model1.HeaderColumn{Name: "TAINTS"},
model1.HeaderColumn{Name: "VERSION"}, model1.HeaderColumn{Name: "VERSION"},
model1.HeaderColumn{Name: "OS-IMAGE", Wide: true}, model1.HeaderColumn{Name: "OS-IMAGE", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "KERNEL", Wide: true}, model1.HeaderColumn{Name: "KERNEL", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "INTERNAL-IP", Wide: true}, model1.HeaderColumn{Name: "INTERNAL-IP", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "EXTERNAL-IP", Wide: true}, model1.HeaderColumn{Name: "EXTERNAL-IP", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "PODS", Align: tview.AlignRight}, model1.HeaderColumn{Name: "PODS", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "CPU", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "MEM", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "%CPU", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "%CPU", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "%MEM", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "%MEM", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "CPU/A", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "CPU/A", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "MEM/A", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "MEM/A", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (n Node) Render(o interface{}, ns string, r *model1.Row) error { func (n Node) Render(o interface{}, ns string, row *model1.Row) error {
oo, ok := o.(*NodeWithMetrics) nwm, ok := o.(*NodeWithMetrics)
if !ok { if !ok {
return fmt.Errorf("expected *NodeAndMetrics, but got %T", o) return fmt.Errorf("expected PodWithMetrics, but got %T", o)
} }
meta, ok := oo.Raw.Object["metadata"].(map[string]interface{}) if err := n.defaultRow(nwm, row); err != nil {
if !ok { return err
return fmt.Errorf("unable to extract meta")
} }
na := extractMetaField(meta, "name") if n.specs.isEmpty() {
return nil
}
cols, err := n.specs.realize(nwm.Raw, n.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
// Render renders a K8s resource to screen.
func (n Node) defaultRow(nwm *NodeWithMetrics, r *model1.Row) error {
var no v1.Node var no v1.Node
err := runtime.DefaultUnstructuredConverter.FromUnstructured(oo.Raw.Object, &no) err := runtime.DefaultUnstructuredConverter.FromUnstructured(nwm.Raw.Object, &no)
if err != nil { if err != nil {
return err return err
} }
@ -76,7 +93,7 @@ func (n Node) Render(o interface{}, ns string, r *model1.Row) error {
iIP, eIP := getIPs(no.Status.Addresses) iIP, eIP := getIPs(no.Status.Addresses)
iIP, eIP = missing(iIP), missing(eIP) iIP, eIP = missing(iIP), missing(eIP)
c, a := gatherNodeMX(&no, oo.MX) c, a := gatherNodeMX(&no, nwm.MX)
statuses := make(sort.StringSlice, 10) statuses := make(sort.StringSlice, 10)
status(no.Status.Conditions, no.Spec.Unschedulable, statuses) status(no.Status.Conditions, no.Spec.Unschedulable, statuses)
sort.Sort(statuses) sort.Sort(statuses)
@ -84,11 +101,11 @@ func (n Node) Render(o interface{}, ns string, r *model1.Row) error {
nodeRoles(&no, roles) nodeRoles(&no, roles)
sort.Sort(roles) sort.Sort(roles)
podCount := strconv.Itoa(oo.PodCount) podCount := strconv.Itoa(nwm.PodCount)
if pc := oo.PodCount; pc == -1 { if pc := nwm.PodCount; pc == -1 {
podCount = NAValue podCount = NAValue
} }
r.ID = client.FQN("", na) r.ID = client.FQN("", no.Name)
r.Fields = model1.Fields{ r.Fields = model1.Fields{
no.Name, no.Name,
join(statuses, ","), join(statuses, ","),

View File

@ -21,28 +21,50 @@ type NetworkPolicy struct {
} }
// Header returns a header row. // Header returns a header row.
func (NetworkPolicy) Header(ns string) model1.Header { func (p NetworkPolicy) Header(_ string) model1.Header {
return p.doHeader(p.defaultHeader())
}
func (NetworkPolicy) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "ING-SELECTOR", Wide: true}, model1.HeaderColumn{Name: "POD-SELECTOR"},
model1.HeaderColumn{Name: "ING-PORTS"}, model1.HeaderColumn{Name: "ING-SELECTOR", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "ING-BLOCK"}, model1.HeaderColumn{Name: "ING-PORTS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "EGR-SELECTOR", Wide: true}, model1.HeaderColumn{Name: "ING-BLOCK", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "EGR-PORTS"}, model1.HeaderColumn{Name: "EGR-SELECTOR", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "EGR-BLOCK"}, model1.HeaderColumn{Name: "EGR-PORTS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "EGR-BLOCK", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (n NetworkPolicy) Render(o interface{}, ns string, r *model1.Row) error { func (p NetworkPolicy) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected NetworkPolicy, but got %T", o) return fmt.Errorf("expected NetworkPolicy, but got %T", o)
} }
if err := p.defaultRow(raw, row); err != nil {
return err
}
if p.specs.isEmpty() {
return nil
}
cols, err := p.specs.realize(raw, p.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (n NetworkPolicy) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var np netv1.NetworkPolicy var np netv1.NetworkPolicy
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &np) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &np)
if err != nil { if err != nil {
@ -52,10 +74,18 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *model1.Row) error {
ip, is, ib := ingress(np.Spec.Ingress) ip, is, ib := ingress(np.Spec.Ingress)
ep, es, eb := egress(np.Spec.Egress) ep, es, eb := egress(np.Spec.Egress)
var podSel string
if len(np.Spec.PodSelector.MatchLabels) > 0 {
podSel = mapToStr(np.Spec.PodSelector.MatchLabels)
}
if len(np.Spec.PodSelector.MatchExpressions) > 0 {
podSel += "::" + expToStr(np.Spec.PodSelector.MatchExpressions)
}
r.ID = client.MetaFQN(np.ObjectMeta) r.ID = client.MetaFQN(np.ObjectMeta)
r.Fields = model1.Fields{ r.Fields = model1.Fields{
np.Namespace, np.Namespace,
np.Name, np.Name,
podSel,
is, is,
ip, ip,
ib, ib,

View File

@ -17,5 +17,5 @@ func TestNetworkPolicyRender(t *testing.T) {
assert.NoError(t, c.Render(load(t, "np"), "", &r)) assert.NoError(t, c.Render(load(t, "np"), "", &r))
assert.Equal(t, "default/fred", r.ID) assert.Equal(t, "default/fred", r.ID)
assert.Equal(t, model1.Fields{"default", "fred", "ns:app=blee,po:app=fred", "TCP:6379", "172.17.0.0/16[172.17.1.0/24,172.17.3.0/24...]", "", "TCP:5978", "10.0.0.0/24"}, r.Fields[:8]) assert.Equal(t, model1.Fields{"default", "fred", "app=nginx", "ns:app=blee,po:app=fred", "TCP:6379", "172.17.0.0/16[172.17.1.0/24,172.17.3.0/24...]", "", "TCP:5978", "10.0.0.0/24"}, r.Fields[:9])
} }

View File

@ -39,23 +39,44 @@ func (n Namespace) ColorerFunc() model1.ColorerFunc {
} }
} }
// Header returns a header rbw. // Header returns a header row.
func (Namespace) Header(string) model1.Header { func (n Namespace) Header(_ string) model1.Header {
return n.doHeader(n.defaultHeader())
}
func (Namespace) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "STATUS"}, model1.HeaderColumn{Name: "STATUS"},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (n Namespace) Render(o interface{}, _ string, r *model1.Row) error { func (n Namespace) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected Namespace, but got %T", o) return fmt.Errorf("expected NetworkPolicy, but got %T", o)
} }
if err := n.defaultRow(raw, row); err != nil {
return err
}
if n.specs.isEmpty() {
return nil
}
cols, err := n.specs.realize(raw, n.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (n Namespace) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var ns v1.Namespace var ns v1.Namespace
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ns) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ns)
if err != nil { if err != nil {

View File

@ -22,28 +22,49 @@ type PodDisruptionBudget struct {
} }
// Header returns a header row. // Header returns a header row.
func (PodDisruptionBudget) Header(ns string) model1.Header { func (p PodDisruptionBudget) Header(_ string) model1.Header {
return p.doHeader(p.defaultHeader())
}
func (PodDisruptionBudget) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "MIN AVAILABLE", Align: tview.AlignRight}, model1.HeaderColumn{Name: "MIN-AVAILABLE", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "MAX UNAVAILABLE", Align: tview.AlignRight}, model1.HeaderColumn{Name: "MAX-UNAVAILABLE", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "ALLOWED DISRUPTIONS", Align: tview.AlignRight}, model1.HeaderColumn{Name: "ALLOWED-DISRUPTIONS", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, model1.HeaderColumn{Name: "CURRENT", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, model1.HeaderColumn{Name: "DESIRED", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "EXPECTED", Align: tview.AlignRight}, model1.HeaderColumn{Name: "EXPECTED", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (p PodDisruptionBudget) Render(o interface{}, ns string, r *model1.Row) error { func (p PodDisruptionBudget) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected PodDisruptionBudget, but got %T", o) return fmt.Errorf("expected PodDisruptionBudget, but got %T", o)
} }
if err := p.defaultRow(raw, row); err != nil {
return err
}
if p.specs.isEmpty() {
return nil
}
cols, err := p.specs.realize(raw, p.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (p PodDisruptionBudget) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var pdb v1.PodDisruptionBudget var pdb v1.PodDisruptionBudget
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pdb) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pdb)
if err != nil { if err != nil {

View File

@ -8,6 +8,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2" "github.com/derailed/tcell/v2"
"github.com/derailed/tview" "github.com/derailed/tview"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
@ -17,9 +19,6 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/model1"
) )
const ( const (
@ -49,7 +48,14 @@ const (
// Pod renders a K8s Pod to screen. // Pod renders a K8s Pod to screen.
type Pod struct { type Pod struct {
Base *Base
}
// NewPod returns a new instance.
func NewPod() *Pod {
return &Pod{
Base: new(Base),
}
} }
// ColorerFunc colors a resource row. // ColorerFunc colors a resource row.
@ -84,33 +90,37 @@ func (p Pod) ColorerFunc() model1.ColorerFunc {
} }
// Header returns a header row. // Header returns a header row.
func (p Pod) Header(ns string) model1.Header { func (p Pod) Header(_ string) model1.Header {
return p.doHeader(p.defaultHeader())
}
func (Pod) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "VS", VS: true}, model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}},
model1.HeaderColumn{Name: "PF"}, model1.HeaderColumn{Name: "PF"},
model1.HeaderColumn{Name: "READY"}, model1.HeaderColumn{Name: "READY"},
model1.HeaderColumn{Name: "STATUS"}, model1.HeaderColumn{Name: "STATUS"},
model1.HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, model1.HeaderColumn{Name: "RESTARTS", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "LAST RESTART", Align: tview.AlignRight, Time: true, Wide: true}, model1.HeaderColumn{Name: "LAST RESTART", Attrs: model1.Attrs{Align: tview.AlignRight, Time: true, Wide: true}},
model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "CPU", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "MEM", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight, Wide: true}, model1.HeaderColumn{Name: "CPU/R:L", Attrs: model1.Attrs{Align: tview.AlignRight, Wide: true}},
model1.HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight, Wide: true}, model1.HeaderColumn{Name: "MEM/R:L", Attrs: model1.Attrs{Align: tview.AlignRight, Wide: true}},
model1.HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "%CPU/R", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "%CPU/L", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "%MEM/R", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "%MEM/L", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "IP"}, model1.HeaderColumn{Name: "IP"},
model1.HeaderColumn{Name: "NODE"}, model1.HeaderColumn{Name: "NODE"},
model1.HeaderColumn{Name: "SERVICEACCOUNT", Wide: true}, model1.HeaderColumn{Name: "SERVICE-ACCOUNT", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "NOMINATED NODE", Wide: true}, model1.HeaderColumn{Name: "NOMINATED NODE", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "READINESS GATES", Wide: true}, model1.HeaderColumn{Name: "READINESS GATES", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "QOS", Wide: true}, model1.HeaderColumn{Name: "QOS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
@ -120,7 +130,24 @@ func (p Pod) Render(o interface{}, ns string, row *model1.Row) error {
if !ok { if !ok {
return fmt.Errorf("expected PodWithMetrics, but got %T", o) return fmt.Errorf("expected PodWithMetrics, but got %T", o)
} }
if err := p.defaultRow(pwm, row); err != nil {
return err
}
if p.specs.isEmpty() {
return nil
}
// !BOZO!! Call header 2 times
cols, err := p.specs.realize(pwm.Raw, p.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (p Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error {
var po v1.Pod var po v1.Pod
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object, &po); err != nil { if err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object, &po); err != nil {
return err return err

View File

@ -157,7 +157,7 @@ func TestPodRender(t *testing.T) {
MX: makePodMX("nginx", "100m", "50Mi"), MX: makePodMX("nginx", "100m", "50Mi"),
} }
var po render.Pod po := render.NewPod()
r := model1.NewRow(14) r := model1.NewRow(14)
err := po.Render(&pom, "", &r) err := po.Render(&pom, "", &r)
assert.Nil(t, err) assert.Nil(t, err)
@ -188,7 +188,7 @@ func TestPodInitRender(t *testing.T) {
MX: makePodMX("nginx", "10m", "10Mi"), MX: makePodMX("nginx", "10m", "10Mi"),
} }
var po render.Pod po := render.NewPod()
r := model1.NewRow(14) r := model1.NewRow(14)
err := po.Render(&pom, "", &r) err := po.Render(&pom, "", &r)
assert.Nil(t, err) assert.Nil(t, err)
@ -204,7 +204,7 @@ func TestPodSidecarRender(t *testing.T) {
MX: makePodMX("sleep", "100m", "40Mi"), MX: makePodMX("sleep", "100m", "40Mi"),
} }
var po render.Pod po := render.NewPod()
r := model1.NewRow(14) r := model1.NewRow(14)
err := po.Render(&pom, "", &r) err := po.Render(&pom, "", &r)
assert.Nil(t, err) assert.Nil(t, err)

View File

@ -24,7 +24,7 @@ func rbacVerbHeader() model1.Header {
model1.HeaderColumn{Name: "UPDATE"}, model1.HeaderColumn{Name: "UPDATE"},
model1.HeaderColumn{Name: "DELETE"}, model1.HeaderColumn{Name: "DELETE"},
model1.HeaderColumn{Name: "DEL-LIST "}, model1.HeaderColumn{Name: "DEL-LIST "},
model1.HeaderColumn{Name: "EXTRAS", Wide: true}, model1.HeaderColumn{Name: "EXTRAS", Attrs: model1.Attrs{Wide: true}},
} }
} }
@ -49,7 +49,7 @@ func (Policy) Header(ns string) model1.Header {
model1.HeaderColumn{Name: "BINDING"}, model1.HeaderColumn{Name: "BINDING"},
} }
h = append(h, rbacVerbHeader()...) h = append(h, rbacVerbHeader()...)
h = append(h, model1.HeaderColumn{Name: "VALID", Wide: true}) h = append(h, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}})
return h return h
} }

View File

@ -59,8 +59,8 @@ func (PortForward) Header(ns string) model1.Header {
model1.HeaderColumn{Name: "URL"}, model1.HeaderColumn{Name: "URL"},
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
model1.HeaderColumn{Name: "N"}, model1.HeaderColumn{Name: "N"},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }

View File

@ -47,30 +47,52 @@ func (p PersistentVolume) ColorerFunc() model1.ColorerFunc {
} }
} }
// Header returns a header rbw. // Header returns a header row.
func (PersistentVolume) Header(string) model1.Header { func (p PersistentVolume) Header(_ string) model1.Header {
return p.doHeader(p.defaultHeader())
}
func (PersistentVolume) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "CAPACITY", Capacity: true}, model1.HeaderColumn{Name: "CAPACITY", Attrs: model1.Attrs{Capacity: true}},
model1.HeaderColumn{Name: "ACCESS MODES"}, model1.HeaderColumn{Name: "ACCESS MODES"},
model1.HeaderColumn{Name: "RECLAIM POLICY"}, model1.HeaderColumn{Name: "RECLAIM POLICY"},
model1.HeaderColumn{Name: "STATUS"}, model1.HeaderColumn{Name: "STATUS"},
model1.HeaderColumn{Name: "CLAIM"}, model1.HeaderColumn{Name: "CLAIM"},
model1.HeaderColumn{Name: "STORAGECLASS"}, model1.HeaderColumn{Name: "STORAGECLASS"},
model1.HeaderColumn{Name: "REASON"}, model1.HeaderColumn{Name: "REASON"},
model1.HeaderColumn{Name: "VOLUMEMODE", Wide: true}, model1.HeaderColumn{Name: "VOLUMEMODE", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (p PersistentVolume) Render(o interface{}, ns string, r *model1.Row) error { func (p PersistentVolume) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected PersistentVolume, but got %T", o) return fmt.Errorf("expected PersistentVolume, but got %T", o)
} }
if err := p.defaultRow(raw, row); err != nil {
return err
}
if p.specs.isEmpty() {
return nil
}
cols, err := p.specs.realize(raw, p.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (p PersistentVolume) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var pv v1.PersistentVolume var pv v1.PersistentVolume
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pv) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pv)
if err != nil { if err != nil {

View File

@ -18,28 +18,50 @@ type PersistentVolumeClaim struct {
Base Base
} }
// Header returns a header rbw. // Header returns a header row.
func (PersistentVolumeClaim) Header(ns string) model1.Header { func (p PersistentVolumeClaim) Header(_ string) model1.Header {
return p.doHeader(p.defaultHeader())
}
func (PersistentVolumeClaim) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "STATUS"}, model1.HeaderColumn{Name: "STATUS"},
model1.HeaderColumn{Name: "VOLUME"}, model1.HeaderColumn{Name: "VOLUME"},
model1.HeaderColumn{Name: "CAPACITY", Capacity: true}, model1.HeaderColumn{Name: "CAPACITY", Attrs: model1.Attrs{Capacity: true}},
model1.HeaderColumn{Name: "ACCESS MODES"}, model1.HeaderColumn{Name: "ACCESS MODES"},
model1.HeaderColumn{Name: "STORAGECLASS"}, model1.HeaderColumn{Name: "STORAGECLASS"},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *model1.Row) error { func (p PersistentVolumeClaim) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected PersistentVolumeClaim, but got %T", o) return fmt.Errorf("expected PersistentVolumeClaim, but got %T", o)
} }
if err := p.defaultRow(raw, row); err != nil {
return err
}
if p.specs.isEmpty() {
return nil
}
cols, err := p.specs.realize(raw, p.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (p PersistentVolumeClaim) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var pvc v1.PersistentVolumeClaim var pvc v1.PersistentVolumeClaim
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pvc) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pvc)
if err != nil { if err != nil {

View File

@ -51,7 +51,7 @@ func (Rbac) Header(ns string) model1.Header {
) )
h = append(h, rbacVerbHeader()...) h = append(h, rbacVerbHeader()...)
return append(h, model1.HeaderColumn{Name: "VALID", Wide: true}) return append(h, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}})
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.

View File

@ -19,26 +19,44 @@ type Role struct {
} }
// Header returns a header row. // Header returns a header row.
func (Role) Header(ns string) model1.Header { func (r Role) Header(_ string) model1.Header {
var h model1.Header return r.doHeader(r.defaultHeader())
if client.IsAllNamespaces(ns) { }
h = append(h, model1.HeaderColumn{Name: "NAMESPACE"})
}
return append(h, func (Role) defaultHeader() model1.Header {
return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
) }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (r Role) Render(o interface{}, ns string, row *model1.Row) error { func (r Role) Render(o interface{}, _ string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected Role, but got %T", o) return fmt.Errorf("expected Role, but got %T", o)
} }
if err := r.defaultRow(raw, row); err != nil {
return err
}
if r.specs.isEmpty() {
return nil
}
cols, err := r.specs.realize(raw, r.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (r Role) defaultRow(raw *unstructured.Unstructured, row *model1.Row) error {
var ro rbacv1.Role var ro rbacv1.Role
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ro) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ro)
if err != nil { if err != nil {
@ -46,16 +64,13 @@ func (r Role) Render(o interface{}, ns string, row *model1.Row) error {
} }
row.ID = client.MetaFQN(ro.ObjectMeta) row.ID = client.MetaFQN(ro.ObjectMeta)
row.Fields = make(model1.Fields, 0, len(r.Header(ns))) row.Fields = model1.Fields{
if client.IsAllNamespaces(ns) { ro.Namespace,
row.Fields = append(row.Fields, ro.Namespace)
}
row.Fields = append(row.Fields,
ro.Name, ro.Name,
mapToStr(ro.Labels), mapToStr(ro.Labels),
"", "",
ToAge(ro.GetCreationTimestamp()), ToAge(ro.GetCreationTimestamp()),
) }
return nil return nil
} }

View File

@ -19,22 +19,22 @@ type RoleBinding struct {
Base Base
} }
// Header returns a header rbw. // Header returns a header row.
func (RoleBinding) Header(ns string) model1.Header { func (r RoleBinding) Header(_ string) model1.Header {
var h model1.Header return r.doHeader(r.defaultHeader())
if client.IsAllNamespaces(ns) { }
h = append(h, model1.HeaderColumn{Name: "NAMESPACE"})
}
return append(h, func (RoleBinding) defaultHeader() model1.Header {
return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "ROLE"}, model1.HeaderColumn{Name: "ROLE"},
model1.HeaderColumn{Name: "KIND"}, model1.HeaderColumn{Name: "KIND"},
model1.HeaderColumn{Name: "SUBJECTS"}, model1.HeaderColumn{Name: "SUBJECTS"},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
) }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
@ -43,6 +43,24 @@ func (r RoleBinding) Render(o interface{}, ns string, row *model1.Row) error {
if !ok { if !ok {
return fmt.Errorf("expected RoleBinding, but got %T", o) return fmt.Errorf("expected RoleBinding, but got %T", o)
} }
if err := r.defaultRow(raw, row); err != nil {
return err
}
if r.specs.isEmpty() {
return nil
}
cols, err := r.specs.realize(raw, r.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (r RoleBinding) defaultRow(raw *unstructured.Unstructured, row *model1.Row) error {
var rb rbacv1.RoleBinding var rb rbacv1.RoleBinding
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rb) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rb)
if err != nil { if err != nil {
@ -52,11 +70,8 @@ func (r RoleBinding) Render(o interface{}, ns string, row *model1.Row) error {
kind, ss := renderSubjects(rb.Subjects) kind, ss := renderSubjects(rb.Subjects)
row.ID = client.MetaFQN(rb.ObjectMeta) row.ID = client.MetaFQN(rb.ObjectMeta)
row.Fields = make(model1.Fields, 0, len(r.Header(ns))) row.Fields = model1.Fields{
if client.IsAllNamespaces(ns) { rb.Namespace,
row.Fields = append(row.Fields, rb.Namespace)
}
row.Fields = append(row.Fields,
rb.Name, rb.Name,
rb.RoleRef.Name, rb.RoleRef.Name,
kind, kind,
@ -64,7 +79,7 @@ func (r RoleBinding) Render(o interface{}, ns string, row *model1.Row) error {
mapToStr(rb.Labels), mapToStr(rb.Labels),
"", "",
ToAge(rb.GetCreationTimestamp()), ToAge(rb.GetCreationTimestamp()),
) }
return nil return nil
} }

View File

@ -27,19 +27,23 @@ func (r ReplicaSet) ColorerFunc() model1.ColorerFunc {
} }
// Header returns a header row. // Header returns a header row.
func (ReplicaSet) Header(ns string) model1.Header { func (r ReplicaSet) Header(_ string) model1.Header {
return r.doHeader(r.defaultHeader())
}
func (ReplicaSet) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "VS", VS: true}, model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}},
model1.HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, model1.HeaderColumn{Name: "DESIRED", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, model1.HeaderColumn{Name: "CURRENT", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "READY", Align: tview.AlignRight}, model1.HeaderColumn{Name: "READY", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "CONTAINERS", Wide: true}, model1.HeaderColumn{Name: "CONTAINERS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "IMAGES", Wide: true}, model1.HeaderColumn{Name: "IMAGES", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "SELECTOR", Wide: true}, model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
@ -50,6 +54,23 @@ func (r ReplicaSet) Render(o interface{}, ns string, row *model1.Row) error {
return fmt.Errorf("expected ReplicaSet, but got %T", o) return fmt.Errorf("expected ReplicaSet, but got %T", o)
} }
if err := r.defaultRow(raw, row); err != nil {
return err
}
if r.specs.isEmpty() {
return nil
}
cols, err := r.specs.realize(raw, r.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (r ReplicaSet) defaultRow(raw *unstructured.Unstructured, row *model1.Row) error {
var rs appsv1.ReplicaSet var rs appsv1.ReplicaSet
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rs) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rs)
if err != nil { if err != nil {

View File

@ -20,23 +20,45 @@ type ServiceAccount struct {
} }
// Header returns a header row. // Header returns a header row.
func (ServiceAccount) Header(ns string) model1.Header { func (s ServiceAccount) Header(_ string) model1.Header {
return s.doHeader(s.defaultHeader())
}
func (ServiceAccount) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "SECRET"}, model1.HeaderColumn{Name: "SECRET"},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (s ServiceAccount) Render(o interface{}, ns string, r *model1.Row) error { func (s ServiceAccount) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected ServiceAccount, but got %T", o) return fmt.Errorf("expected ServiceAccount, but got %T", o)
} }
if err := s.defaultRow(raw, row); err != nil {
return err
}
if s.specs.isEmpty() {
return nil
}
cols, err := s.specs.realize(raw, s.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (s ServiceAccount) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var sa v1.ServiceAccount var sa v1.ServiceAccount
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sa) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sa)
if err != nil { if err != nil {

View File

@ -21,25 +21,47 @@ type StorageClass struct {
} }
// Header returns a header row. // Header returns a header row.
func (StorageClass) Header(ns string) model1.Header { func (s StorageClass) Header(_ string) model1.Header {
return s.doHeader(s.defaultHeader())
}
func (StorageClass) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "PROVISIONER"}, model1.HeaderColumn{Name: "PROVISIONER"},
model1.HeaderColumn{Name: "RECLAIMPOLICY"}, model1.HeaderColumn{Name: "RECLAIMPOLICY"},
model1.HeaderColumn{Name: "VOLUMEBINDINGMODE"}, model1.HeaderColumn{Name: "VOLUMEBINDINGMODE"},
model1.HeaderColumn{Name: "ALLOWVOLUMEEXPANSION"}, model1.HeaderColumn{Name: "ALLOWVOLUMEEXPANSION"},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (s StorageClass) Render(o interface{}, ns string, r *model1.Row) error { func (s StorageClass) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected StorageClass, but got %T", o) return fmt.Errorf("expected StorageClass, but got %T", o)
} }
if err := s.defaultRow(raw, row); err != nil {
return err
}
if s.specs.isEmpty() {
return nil
}
cols, err := s.specs.realize(raw, s.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (s StorageClass) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var sc storagev1.StorageClass var sc storagev1.StorageClass
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sc) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sc)
if err != nil { if err != nil {

View File

@ -33,8 +33,8 @@ func (ScreenDump) Header(ns string) model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "DIR"}, model1.HeaderColumn{Name: "DIR"},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }

View File

@ -20,24 +20,46 @@ type Secret struct {
Base Base
} }
// Header returns a header rbw. // Header returns a header row.
func (Secret) Header(string) model1.Header { func (s Secret) Header(_ string) model1.Header {
return s.doHeader(s.defaultHeader())
}
func (Secret) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "TYPE"}, model1.HeaderColumn{Name: "TYPE"},
model1.HeaderColumn{Name: "DATA"}, model1.HeaderColumn{Name: "DATA"},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (n Secret) Render(o interface{}, _ string, r *model1.Row) error { func (s Secret) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected Secret, but got %T", o) return fmt.Errorf("expected Secret, but got %T", o)
} }
if err := s.defaultRow(raw, row); err != nil {
return err
}
if s.specs.isEmpty() {
return nil
}
cols, err := s.specs.realize(raw, s.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (n Secret) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var sec v1.Secret var sec v1.Secret
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sec) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sec)
if err != nil { if err != nil {

View File

@ -20,28 +20,50 @@ type StatefulSet struct {
} }
// Header returns a header row. // Header returns a header row.
func (StatefulSet) Header(ns string) model1.Header { func (s StatefulSet) Header(_ string) model1.Header {
return s.doHeader(s.defaultHeader())
}
func (StatefulSet) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "VS", VS: true}, model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}},
model1.HeaderColumn{Name: "READY"}, model1.HeaderColumn{Name: "READY"},
model1.HeaderColumn{Name: "SELECTOR", Wide: true}, model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "SERVICE"}, model1.HeaderColumn{Name: "SERVICE"},
model1.HeaderColumn{Name: "CONTAINERS", Wide: true}, model1.HeaderColumn{Name: "CONTAINERS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "IMAGES", Wide: true}, model1.HeaderColumn{Name: "IMAGES", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (s StatefulSet) Render(o interface{}, ns string, r *model1.Row) error { func (s StatefulSet) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected StatefulSet, but got %T", o) return fmt.Errorf("expected StatefulSet, but got %T", o)
} }
if err := s.defaultRow(raw, row); err != nil {
return err
}
if s.specs.isEmpty() {
return nil
}
cols, err := s.specs.realize(raw, s.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (s StatefulSet) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var sts appsv1.StatefulSet var sts appsv1.StatefulSet
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sts) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sts)
if err != nil { if err != nil {

View File

@ -30,7 +30,7 @@ func (Subject) Header(ns string) model1.Header {
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "KIND"}, model1.HeaderColumn{Name: "KIND"},
model1.HeaderColumn{Name: "FIRST LOCATION"}, model1.HeaderColumn{Name: "FIRST LOCATION"},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
} }
} }

View File

@ -22,27 +22,50 @@ type Service struct {
} }
// Header returns a header row. // Header returns a header row.
func (Service) Header(ns string) model1.Header { func (s Service) Header(_ string) model1.Header {
return s.doHeader(s.defaultHeader())
}
// Header returns a header row.
func (Service) defaultHeader() model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "TYPE"}, model1.HeaderColumn{Name: "TYPE"},
model1.HeaderColumn{Name: "CLUSTER-IP"}, model1.HeaderColumn{Name: "CLUSTER-IP"},
model1.HeaderColumn{Name: "EXTERNAL-IP"}, model1.HeaderColumn{Name: "EXTERNAL-IP"},
model1.HeaderColumn{Name: "SELECTOR", Wide: true}, model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "PORTS", Wide: false}, model1.HeaderColumn{Name: "PORTS", Attrs: model1.Attrs{Wide: false}},
model1.HeaderColumn{Name: "LABELS", Wide: true}, model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (s Service) Render(o interface{}, ns string, r *model1.Row) error { func (s Service) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("expected Service, but got %T", o) return fmt.Errorf("expected Service, but got %T", o)
} }
if err := s.defaultRow(raw, row); err != nil {
return err
}
if s.specs.isEmpty() {
return nil
}
cols, err := s.specs.realize(raw, s.defaultHeader(), row)
if err != nil {
return err
}
cols.hydrateRow(row)
return nil
}
func (s Service) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {
var svc v1.Service var svc v1.Service
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &svc) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &svc)
if err != nil { if err != nil {

168
internal/render/table.go Normal file
View File

@ -0,0 +1,168 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package render
import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/model1"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const ageTableCol = "Age"
// Table renders a tabular resource to screen.
type Table struct {
Base
table *metav1.Table
header model1.Header
ageIndex int
}
func (*Table) IsGeneric() bool {
return true
}
// SetTable sets the tabular resource.
func (t *Table) SetTable(ns string, table *metav1.Table) {
t.table = table
t.header = t.Header(ns)
}
// ColorerFunc colors a resource row.
func (*Table) ColorerFunc() model1.ColorerFunc {
return model1.DefaultColorer
}
// Header returns a header row.
func (t *Table) Header(ns string) model1.Header {
return t.doHeader(t.defaultHeader())
}
// Header returns a header row.
func (t *Table) defaultHeader() model1.Header {
if t.table == nil {
return model1.Header{}
}
h := make(model1.Header, 0, len(t.table.ColumnDefinitions))
for i, c := range t.table.ColumnDefinitions {
if c.Name == ageTableCol {
t.ageIndex = i
continue
}
h = append(h, model1.HeaderColumn{Name: strings.ToUpper(c.Name)})
}
if t.ageIndex > 0 {
h = append(h, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}})
}
return h
}
// Render renders a K8s resource to screen.
func (t *Table) Render(o any, ns string, r *model1.Row) error {
row, ok := o.(metav1.TableRow)
if !ok {
return fmt.Errorf("expected Table, but got %T", o)
}
if err := t.defaultRow(&row, ns, r); err != nil {
return err
}
if t.specs.isEmpty() {
return nil
}
cols, err := t.specs.realize(row.Object.Object, t.defaultHeader(), r)
if err != nil {
return err
}
cols.hydrateRow(r)
return nil
}
func (t *Table) defaultRow(row *metav1.TableRow, ns string, r *model1.Row) error {
th := t.defaultHeader()
ons, name := ns, UnknownValue
if row.Object.Object != nil {
m, _ := meta.Accessor(row.Object.Object)
if m != nil {
ons, name = m.GetNamespace(), m.GetName()
}
} else if idx, ok := th.IndexOf("NAME", true); ok && idx >= 0 {
name = row.Cells[idx].(string)
if idx, ok := th.IndexOf("NAMESPACE", true); ok && idx >= 0 {
ons = row.Cells[idx].(string)
}
} else {
}
if client.IsClusterWide(ons) {
ons = client.ClusterScope
}
r.ID = client.FQN(ons, name)
r.Fields = make(model1.Fields, 0, len(th))
var age any
for i, c := range row.Cells {
if t.ageIndex > 0 && i == t.ageIndex {
age = c
continue
}
if c == nil {
r.Fields = append(r.Fields, Blank)
continue
}
r.Fields = append(r.Fields, fmt.Sprintf("%v", c))
}
if d, ok := age.(string); ok {
r.Fields = append(r.Fields, d)
} else if t.ageIndex > 0 {
log.Warn().Msgf("No Duration detected on age field")
r.Fields = append(r.Fields, NAValue)
}
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
func resourceNS(raw []byte) (string, string, error) {
var obj map[string]interface{}
var ns, name string
err := json.Unmarshal(raw, &obj)
if err != nil {
return ns, name, err
}
meta, ok := obj["metadata"].(map[string]interface{})
if !ok {
return ns, name, errors.New("no metadata found on generic resource")
}
ina, ok := meta["name"]
if !ok {
return ns, name, errors.New("unable to extract resource name")
}
name, ok = ina.(string)
if !ok {
return ns, name, fmt.Errorf("expecting name string type but got %T", ns)
}
ins, ok := meta["namespace"]
if !ok {
return client.ClusterScope, name, nil
}
ns, ok = ins.(string)
if !ok {
return ns, name, fmt.Errorf("expecting namespace string type but got %T", ns)
}
return ns, name, nil
}

View File

@ -7,9 +7,11 @@ import (
"testing" "testing"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
cfg "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
@ -34,6 +36,7 @@ func TestGenericRender(t *testing.T) {
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
}, },
}, },
"all": { "all": {
ns: client.NamespaceAll, ns: client.NamespaceAll,
table: makeNSGeneric(), table: makeNSGeneric(),
@ -46,18 +49,7 @@ func TestGenericRender(t *testing.T) {
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
}, },
}, },
"allNS": {
ns: client.NamespaceAll,
table: makeNSGeneric(),
eID: "ns1/fred",
eFields: model1.Fields{"ns1", "c1", "c2", "c3"},
eHeader: model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "B"},
model1.HeaderColumn{Name: "C"},
},
},
"clusterWide": { "clusterWide": {
ns: client.ClusterScope, ns: client.ClusterScope,
table: makeNoNSGeneric(), table: makeNoNSGeneric(),
@ -69,6 +61,7 @@ func TestGenericRender(t *testing.T) {
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
}, },
}, },
"age": { "age": {
ns: client.ClusterScope, ns: client.ClusterScope,
table: makeAgeGeneric(), table: makeAgeGeneric(),
@ -77,13 +70,13 @@ func TestGenericRender(t *testing.T) {
eHeader: model1.Header{ eHeader: model1.Header{
model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "C"}, model1.HeaderColumn{Name: "C"},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
}, },
}, },
} }
for k := range uu { for k := range uu {
var re render.Generic var re render.Table
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
var r model1.Row var r model1.Row
@ -97,12 +90,60 @@ func TestGenericRender(t *testing.T) {
} }
} }
func TestGenericCustRender(t *testing.T) {
uu := map[string]struct {
ns string
table *metav1beta1.Table
vs cfg.ViewSetting
eID string
eFields model1.Fields
eHeader model1.Header
}{
"spec": {
ns: "ns1",
table: makeNSGeneric(),
vs: cfg.ViewSetting{
Columns: []string{
"NAMESPACE",
"BLEE:.metadata.name",
"ZORG:.metadata.namespace",
},
},
eID: "ns1/fred",
eFields: model1.Fields{"ns1", "fred", "ns1", "c1", "c2", "c3"},
eHeader: model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "BLEE"},
model1.HeaderColumn{Name: "ZORG"},
model1.HeaderColumn{Name: "A"},
model1.HeaderColumn{Name: "B"},
model1.HeaderColumn{Name: "C"},
},
},
}
for k, u := range uu {
var re render.Table
re.SetViewSetting(&u.vs)
t.Run(k, func(t *testing.T) {
var r model1.Row
re.SetTable(u.ns, u.table)
assert.Equal(t, u.eHeader, re.Header(u.ns))
assert.Nil(t, re.Render(u.table.Rows[0], u.ns, &r))
assert.Equal(t, u.eID, r.ID)
assert.Equal(t, u.eFields, r.Fields)
})
}
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
func makeNSGeneric() *metav1beta1.Table { func makeNSGeneric() *metav1beta1.Table {
return &metav1beta1.Table{ return &metav1beta1.Table{
ColumnDefinitions: []metav1beta1.TableColumnDefinition{ ColumnDefinitions: []metav1beta1.TableColumnDefinition{
{Name: "NAMESPACE"},
{Name: "a"}, {Name: "a"},
{Name: "b"}, {Name: "b"},
{Name: "c"}, {Name: "c"},
@ -110,15 +151,19 @@ func makeNSGeneric() *metav1beta1.Table {
Rows: []metav1beta1.TableRow{ Rows: []metav1beta1.TableRow{
{ {
Object: runtime.RawExtension{ Object: runtime.RawExtension{
Raw: []byte(`{ Object: &unstructured.Unstructured{
"kind": "fred", Object: map[string]interface{}{
"apiVersion": "v1", "kind": "fred",
"metadata": { "apiVersion": "v1",
"namespace": "ns1", "metadata": map[string]interface{}{
"name": "fred" "namespace": "ns1",
}}`), "name": "fred",
},
},
},
}, },
Cells: []interface{}{ Cells: []interface{}{
"ns1",
"c1", "c1",
"c2", "c2",
"c3", "c3",
@ -138,12 +183,15 @@ func makeNoNSGeneric() *metav1beta1.Table {
Rows: []metav1beta1.TableRow{ Rows: []metav1beta1.TableRow{
{ {
Object: runtime.RawExtension{ Object: runtime.RawExtension{
Raw: []byte(`{ Object: &unstructured.Unstructured{
"kind": "fred", Object: map[string]interface{}{
"apiVersion": "v1", "kind": "fred",
"metadata": { "apiVersion": "v1",
"name": "fred" "metadata": map[string]interface{}{
}}`), "name": "fred",
},
},
},
}, },
Cells: []interface{}{ Cells: []interface{}{
"c1", "c1",
@ -165,12 +213,15 @@ func makeAgeGeneric() *metav1beta1.Table {
Rows: []metav1beta1.TableRow{ Rows: []metav1beta1.TableRow{
{ {
Object: runtime.RawExtension{ Object: runtime.RawExtension{
Raw: []byte(`{ Object: &unstructured.Unstructured{
"kind": "fred", Object: map[string]interface{}{
"apiVersion": "v1", "kind": "fred",
"metadata": { "apiVersion": "v1",
"name": "fred" "metadata": map[string]interface{}{
}}`), "name": "fred",
},
},
},
}, },
Cells: []interface{}{ Cells: []interface{}{
"c1", "c1",

View File

@ -45,8 +45,8 @@ func (Workload) Header(string) model1.Header {
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "STATUS"}, model1.HeaderColumn{Name: "STATUS"},
model1.HeaderColumn{Name: "READY"}, model1.HeaderColumn{Name: "READY"},
model1.HeaderColumn{Name: "VALID", Wide: true}, model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
} }
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
@ -37,12 +38,12 @@ type (
// Table represents tabular data. // Table represents tabular data.
type Table struct { type Table struct {
gvr client.GVR
sortCol model1.SortColumn
manualSort bool
Path string
Extras string
*SelectTable *SelectTable
gvr client.GVR
sortCol model1.SortColumn
manualSort bool
Path string
Extras string
actions *KeyActions actions *KeyActions
cmdBuff *model.FishBuff cmdBuff *model.FishBuff
styles *config.Styles styles *config.Styles
@ -64,6 +65,7 @@ func NewTable(gvr client.GVR) *Table {
model: model.NewTable(gvr), model: model.NewTable(gvr),
marks: make(map[string]struct{}), marks: make(map[string]struct{}),
}, },
ctx: context.Background(),
gvr: gvr, gvr: gvr,
actions: NewKeyActions(), actions: NewKeyActions(),
cmdBuff: model.NewFishBuff('/', model.FilterBuffer), cmdBuff: model.NewFishBuff('/', model.FilterBuffer),
@ -106,19 +108,20 @@ func (t *Table) getMSort() bool {
return t.manualSort return t.manualSort
} }
func (t *Table) setVs(vs *config.ViewSetting) bool { func (t *Table) setViewSetting(vs *config.ViewSetting) bool {
t.mx.Lock() t.mx.Lock()
defer t.mx.Unlock() defer t.mx.Unlock()
if !t.viewSetting.Equals(vs) { if !t.viewSetting.Equals(vs) {
t.viewSetting = vs t.viewSetting = vs
t.model.SetViewSetting(t.ctx, vs)
return true return true
} }
return false return false
} }
func (t *Table) getVs() *config.ViewSetting { func (t *Table) getViewSetting() *config.ViewSetting {
t.mx.RLock() t.mx.RLock()
defer t.mx.RUnlock() defer t.mx.RUnlock()
@ -143,9 +146,6 @@ func (t *Table) Init(ctx context.Context) {
t.SetSelectionChangedFunc(t.selectionChanged) t.SetSelectionChangedFunc(t.selectionChanged)
t.SetBackgroundColor(tcell.ColorDefault) t.SetBackgroundColor(tcell.ColorDefault)
t.Select(1, 0) t.Select(1, 0)
if cfg, ok := ctx.Value(internal.KeyViewConfig).(*config.CustomView); ok && cfg != nil {
cfg.AddListener(t.GVR().String(), t)
}
t.styles = mustExtractStyles(ctx) t.styles = mustExtractStyles(ctx)
t.StylesChanged(t.styles) t.StylesChanged(t.styles)
} }
@ -154,8 +154,11 @@ func (t *Table) Init(ctx context.Context) {
func (t *Table) GVR() client.GVR { return t.gvr } func (t *Table) GVR() client.GVR { return t.gvr }
// ViewSettingsChanged notifies listener the view configuration changed. // ViewSettingsChanged notifies listener the view configuration changed.
func (t *Table) ViewSettingsChanged(vs config.ViewSetting) { func (t *Table) ViewSettingsChanged(vs *config.ViewSetting) {
if t.setVs(&vs) { if t.setViewSetting(vs) {
if vs == nil {
t.setSortCol(model1.SortColumn{})
}
t.setMSort(false) t.setMSort(false)
t.Refresh() t.Refresh()
} }
@ -274,7 +277,7 @@ func (t *Table) doUpdate(data *model1.TableData) *model1.TableData {
t.actions.Delete(KeyShiftP) t.actions.Delete(KeyShiftP)
} }
cdata, sortCol := data.Customize(t.getVs(), t.getSortCol(), t.getMSort(), true) cdata, sortCol := data.Customize(t.getViewSetting(), t.getSortCol(), t.getMSort())
t.setSortCol(sortCol) t.setSortCol(sortCol)
return cdata return cdata
@ -285,12 +288,17 @@ func (t *Table) UpdateUI(cdata, data *model1.TableData) {
fg := t.styles.Table().Header.FgColor.Color() fg := t.styles.Table().Header.FgColor.Color()
bg := t.styles.Table().Header.BgColor.Color() bg := t.styles.Table().Header.BgColor.Color()
var isNamespaced bool
if m, err := dao.MetaAccess.MetaFor(t.GVR()); err == nil {
isNamespaced = m.Namespaced
}
var col int var col int
for _, h := range cdata.Header() { for _, h := range cdata.Header() {
if !t.wide && h.Wide { if h.Hide || (!t.wide && h.Wide) {
continue continue
} }
if h.Name == "NAMESPACE" && !t.GetModel().ClusterWide() { if h.Name == "NAMESPACE" && (!t.GetModel().ClusterWide() || !isNamespaced) {
continue continue
} }
if h.MX && !t.hasMetrics { if h.MX && !t.hasMetrics {
@ -316,7 +324,7 @@ func (t *Table) UpdateUI(cdata, data *model1.TableData) {
log.Error().Msgf("unable to find original re: %q", re.Row.ID) log.Error().Msgf("unable to find original re: %q", re.Row.ID)
return true return true
} }
t.buildRow(row+1, re, ore, cdata.Header(), pads) t.buildRow(row+1, re, ore, cdata.Header(), pads, isNamespaced)
return true return true
}) })
@ -325,7 +333,7 @@ func (t *Table) UpdateUI(cdata, data *model1.TableData) {
t.UpdateTitle() t.UpdateTitle()
} }
func (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads MaxyPad) { func (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads MaxyPad, isNamespaced bool) {
color := model1.DefaultColorer color := model1.DefaultColorer
if t.colorerFn != nil { if t.colorerFn != nil {
color = t.colorerFn color = t.colorerFn
@ -339,11 +347,11 @@ func (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads M
log.Error().Msgf("field/header overflow detected for %q -- %d::%d. Check your mappings!", t.GVR(), c, len(h)) log.Error().Msgf("field/header overflow detected for %q -- %d::%d. Check your mappings!", t.GVR(), c, len(h))
continue continue
} }
if !t.wide && h[c].Wide { if h[c].Hide || (!t.wide && h[c].Wide) {
continue continue
} }
if h[c].Name == "NAMESPACE" && !t.GetModel().ClusterWide() { if h[c].Name == "NAMESPACE" && (!t.GetModel().ClusterWide() || !isNamespaced) {
continue continue
} }
if h[c].MX && !t.hasMetrics { if h[c].MX && !t.hasMetrics {
@ -354,7 +362,14 @@ func (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads M
} }
if !re.Deltas.IsBlank() && !h.IsTimeCol(c) { if !re.Deltas.IsBlank() && !h.IsTimeCol(c) {
field += Deltas(re.Deltas[c], field) var old string
if c < len(ore.Deltas) {
old = ore.Deltas[c]
}
if c < len(re.Deltas) {
old = re.Deltas[c]
}
field += Deltas(old, field)
} }
if h[c].Decorator != nil { if h[c].Decorator != nil {

View File

@ -70,21 +70,22 @@ type mockModel struct{}
var _ ui.Tabular = &mockModel{} var _ ui.Tabular = &mockModel{}
func (t *mockModel) SetInstance(string) {} func (t *mockModel) SetViewSetting(context.Context, *config.ViewSetting) {}
func (t *mockModel) SetLabelFilter(string) {} func (t *mockModel) SetInstance(string) {}
func (t *mockModel) GetLabelFilter() string { return "" } func (t *mockModel) SetLabelFilter(string) {}
func (t *mockModel) Empty() bool { return false } func (t *mockModel) GetLabelFilter() string { return "" }
func (t *mockModel) RowCount() int { return 1 } func (t *mockModel) Empty() bool { return false }
func (t *mockModel) HasMetrics() bool { return true } func (t *mockModel) RowCount() int { return 1 }
func (t *mockModel) Peek() *model1.TableData { return makeTableData() } func (t *mockModel) HasMetrics() bool { return true }
func (t *mockModel) Refresh(context.Context) error { return nil } func (t *mockModel) Peek() *model1.TableData { return makeTableData() }
func (t *mockModel) ClusterWide() bool { return false } func (t *mockModel) Refresh(context.Context) error { return nil }
func (t *mockModel) GetNamespace() string { return "blee" } func (t *mockModel) ClusterWide() bool { return false }
func (t *mockModel) SetNamespace(string) {} func (t *mockModel) GetNamespace() string { return "blee" }
func (t *mockModel) ToggleToast() {} func (t *mockModel) SetNamespace(string) {}
func (t *mockModel) AddListener(model.TableListener) {} func (t *mockModel) ToggleToast() {}
func (t *mockModel) RemoveListener(model.TableListener) {} func (t *mockModel) AddListener(model.TableListener) {}
func (t *mockModel) Watch(context.Context) error { return nil } func (t *mockModel) RemoveListener(model.TableListener) {}
func (t *mockModel) Watch(context.Context) error { return nil }
func (t *mockModel) Get(ctx context.Context, path string) (runtime.Object, error) { func (t *mockModel) Get(ctx context.Context, path string) (runtime.Object, error) {
return nil, nil return nil, nil
} }

View File

@ -7,6 +7,7 @@ import (
"context" "context"
"time" "time"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/model1"
@ -75,4 +76,7 @@ type Tabular interface {
// Delete a resource. // Delete a resource.
Delete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error Delete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error
// SetViewSetting injects custom cols specification.
SetViewSetting(context.Context, *config.ViewSetting)
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/config/mock"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
@ -85,25 +86,26 @@ var (
_ ui.Suggester = (*mockModel)(nil) _ ui.Suggester = (*mockModel)(nil)
) )
func (t *mockModel) CurrentSuggestion() (string, bool) { return "", false } func (t *mockModel) SetViewSetting(context.Context, *config.ViewSetting) {}
func (t *mockModel) NextSuggestion() (string, bool) { return "", false } func (t *mockModel) CurrentSuggestion() (string, bool) { return "", false }
func (t *mockModel) PrevSuggestion() (string, bool) { return "", false } func (t *mockModel) NextSuggestion() (string, bool) { return "", false }
func (t *mockModel) ClearSuggestions() {} func (t *mockModel) PrevSuggestion() (string, bool) { return "", false }
func (t *mockModel) SetInstance(string) {} func (t *mockModel) ClearSuggestions() {}
func (t *mockModel) SetLabelFilter(string) {} func (t *mockModel) SetInstance(string) {}
func (t *mockModel) GetLabelFilter() string { return "" } func (t *mockModel) SetLabelFilter(string) {}
func (t *mockModel) Empty() bool { return false } func (t *mockModel) GetLabelFilter() string { return "" }
func (t *mockModel) RowCount() int { return 1 } func (t *mockModel) Empty() bool { return false }
func (t *mockModel) HasMetrics() bool { return true } func (t *mockModel) RowCount() int { return 1 }
func (t *mockModel) Peek() *model1.TableData { return makeTableData() } func (t *mockModel) HasMetrics() bool { return true }
func (t *mockModel) ClusterWide() bool { return false } func (t *mockModel) Peek() *model1.TableData { return makeTableData() }
func (t *mockModel) GetNamespace() string { return "blee" } func (t *mockModel) ClusterWide() bool { return false }
func (t *mockModel) SetNamespace(string) {} func (t *mockModel) GetNamespace() string { return "blee" }
func (t *mockModel) ToggleToast() {} func (t *mockModel) SetNamespace(string) {}
func (t *mockModel) AddListener(model.TableListener) {} func (t *mockModel) ToggleToast() {}
func (t *mockModel) RemoveListener(model.TableListener) {} func (t *mockModel) AddListener(model.TableListener) {}
func (t *mockModel) Watch(context.Context) error { return nil } func (t *mockModel) RemoveListener(model.TableListener) {}
func (t *mockModel) Refresh(context.Context) error { return nil } func (t *mockModel) Watch(context.Context) error { return nil }
func (t *mockModel) Refresh(context.Context) error { return nil }
func (t *mockModel) Get(context.Context, string) (runtime.Object, error) { func (t *mockModel) Get(context.Context, string) (runtime.Object, error) {
return nil, nil return nil, nil
} }

View File

@ -8,11 +8,11 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"slices"
"strings" "strings"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog" "github.com/derailed/k9s/internal/ui/dialog"
"github.com/derailed/tcell/v2" "github.com/derailed/tcell/v2"
@ -173,7 +173,7 @@ func isKustomized(sel string) bool {
} }
kk := []string{kustomizeNoExt, kustomizeYAML, kustomizeYML} kk := []string{kustomizeNoExt, kustomizeYAML, kustomizeYML}
for _, f := range ff { for _, f := range ff {
if data.InList(kk, f.Name()) { if slices.Contains(kk, f.Name()) {
return true return true
} }
} }

View File

@ -90,10 +90,6 @@ func miscViewers(vv MetaViewers) {
vv[client.NewGVR("pulses")] = MetaViewer{ vv[client.NewGVR("pulses")] = MetaViewer{
viewerFn: NewPulse, viewerFn: NewPulse,
} }
// !!BOZO!! Popeye
// vv[client.NewGVR("popeye")] = MetaViewer{
// viewerFn: NewPopeye,
// }
vv[client.NewGVR("sanitizer")] = MetaViewer{ vv[client.NewGVR("sanitizer")] = MetaViewer{
viewerFn: NewSanitizer, viewerFn: NewSanitizer,
} }

View File

@ -47,7 +47,9 @@ func (t *Table) Init(ctx context.Context) (err error) {
if t.app.Conn() != nil { if t.app.Conn() != nil {
ctx = context.WithValue(ctx, internal.KeyHasMetrics, t.app.Conn().HasMetrics()) ctx = context.WithValue(ctx, internal.KeyHasMetrics, t.app.Conn().HasMetrics())
} }
t.app.CustomView = config.NewCustomView() if t.app.CustomView == nil {
t.app.CustomView = config.NewCustomView()
}
ctx = context.WithValue(ctx, internal.KeyStyles, t.app.Styles) ctx = context.WithValue(ctx, internal.KeyStyles, t.app.Styles)
ctx = context.WithValue(ctx, internal.KeyViewConfig, t.app.CustomView) ctx = context.WithValue(ctx, internal.KeyViewConfig, t.app.CustomView)
t.Table.Init(ctx) t.Table.Init(ctx)
@ -142,12 +144,14 @@ func (t *Table) Start() {
t.Stop() t.Stop()
t.CmdBuff().AddListener(t) t.CmdBuff().AddListener(t)
t.Styles().AddListener(t.Table) t.Styles().AddListener(t.Table)
t.App().CustomView.AddListener(t.Table.GVR().String(), t.Table)
} }
// Stop terminates the component. // Stop terminates the component.
func (t *Table) Stop() { func (t *Table) Stop() {
t.CmdBuff().RemoveListener(t) t.CmdBuff().RemoveListener(t)
t.Styles().RemoveListener(t.Table) t.Styles().RemoveListener(t.Table)
t.App().CustomView.RemoveListener(t.GVR().String())
} }
// SetEnterFn specifies the default enter behavior. // SetEnterFn specifies the default enter behavior.

View File

@ -48,9 +48,9 @@ func TestTableNew(t *testing.T) {
client.NewGVR("test"), client.NewGVR("test"),
model1.Header{ model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME", Align: tview.AlignRight}, model1.HeaderColumn{Name: "NAME", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "FRED"}, model1.HeaderColumn{Name: "FRED"},
model1.HeaderColumn{Name: "AGE", Time: true, Decorator: render.AgeDecorator}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true, Decorator: render.AgeDecorator}},
}, },
model1.NewRowEventsWithEvts( model1.NewRowEventsWithEvts(
model1.RowEvent{ model1.RowEvent{
@ -132,21 +132,22 @@ type mockTableModel struct{}
var _ ui.Tabular = (*mockTableModel)(nil) var _ ui.Tabular = (*mockTableModel)(nil)
func (t *mockTableModel) SetInstance(string) {} func (t *mockTableModel) SetViewSetting(context.Context, *config.ViewSetting) {}
func (t *mockTableModel) SetLabelFilter(string) {} func (t *mockTableModel) SetInstance(string) {}
func (t *mockTableModel) GetLabelFilter() string { return "" } func (t *mockTableModel) SetLabelFilter(string) {}
func (t *mockTableModel) Empty() bool { return false } func (t *mockTableModel) GetLabelFilter() string { return "" }
func (t *mockTableModel) RowCount() int { return 1 } func (t *mockTableModel) Empty() bool { return false }
func (t *mockTableModel) HasMetrics() bool { return true } func (t *mockTableModel) RowCount() int { return 1 }
func (t *mockTableModel) Peek() *model1.TableData { return makeTableData() } func (t *mockTableModel) HasMetrics() bool { return true }
func (t *mockTableModel) Refresh(context.Context) error { return nil } func (t *mockTableModel) Peek() *model1.TableData { return makeTableData() }
func (t *mockTableModel) ClusterWide() bool { return false } func (t *mockTableModel) Refresh(context.Context) error { return nil }
func (t *mockTableModel) GetNamespace() string { return "blee" } func (t *mockTableModel) ClusterWide() bool { return false }
func (t *mockTableModel) SetNamespace(string) {} func (t *mockTableModel) GetNamespace() string { return "blee" }
func (t *mockTableModel) ToggleToast() {} func (t *mockTableModel) SetNamespace(string) {}
func (t *mockTableModel) AddListener(model.TableListener) {} func (t *mockTableModel) ToggleToast() {}
func (t *mockTableModel) RemoveListener(model.TableListener) {} func (t *mockTableModel) AddListener(model.TableListener) {}
func (t *mockTableModel) Watch(context.Context) error { return nil } func (t *mockTableModel) RemoveListener(model.TableListener) {}
func (t *mockTableModel) Watch(context.Context) error { return nil }
func (t *mockTableModel) Get(context.Context, string) (runtime.Object, error) { func (t *mockTableModel) Get(context.Context, string) (runtime.Object, error) {
return nil, nil return nil, nil
} }
@ -171,9 +172,9 @@ func makeTableData() *model1.TableData {
client.NewGVR("test"), client.NewGVR("test"),
model1.Header{ model1.Header{
model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAMESPACE"},
model1.HeaderColumn{Name: "NAME", Align: tview.AlignRight}, model1.HeaderColumn{Name: "NAME", Attrs: model1.Attrs{Align: tview.AlignRight}},
model1.HeaderColumn{Name: "FRED"}, model1.HeaderColumn{Name: "FRED"},
model1.HeaderColumn{Name: "AGE", Time: true}, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
}, },
model1.NewRowEventsWithEvts( model1.NewRowEventsWithEvts(
model1.RowEvent{ model1.RowEvent{

View File

@ -13,7 +13,6 @@ import (
"github.com/anchore/clio" "github.com/anchore/clio"
"github.com/anchore/grype/cmd/grype/cli/options" "github.com/anchore/grype/cmd/grype/cli/options"
"github.com/anchore/grype/grype" "github.com/anchore/grype/grype"
"github.com/anchore/grype/grype/db/legacy/distribution" "github.com/anchore/grype/grype/db/legacy/distribution"
v5 "github.com/anchore/grype/grype/db/v5" v5 "github.com/anchore/grype/grype/db/v5"
"github.com/anchore/grype/grype/db/v5/matcher" "github.com/anchore/grype/grype/db/v5/matcher"
@ -161,7 +160,7 @@ func (s *imageScanner) scanWorker(ctx context.Context, img string) {
} }
} }
func (s *imageScanner) scan(ctx context.Context, img string, sc *Scan) error { func (s *imageScanner) scan(_ context.Context, img string, sc *Scan) error {
defer func(t time.Time) { defer func(t time.Time) {
log.Debug().Msgf("ScanTime %q: %v", img, time.Since(t)) log.Debug().Msgf("ScanTime %q: %v", img, time.Since(t))
}(time.Now()) }(time.Now())

View File

@ -1,6 +1,6 @@
name: k9s name: k9s
base: core22 base: core22
version: 'v0.32.7' version: 'v0.40.0'
summary: K9s is a CLI to view and manage your Kubernetes clusters. summary: K9s is a CLI to view and manage your Kubernetes clusters.
description: | description: |
K9s is a CLI to view and manage your Kubernetes clusters. By leveraging a terminal UI, you can easily traverse Kubernetes resources and view the state of your clusters in a single powerful session. K9s is a CLI to view and manage your Kubernetes clusters. By leveraging a terminal UI, you can easily traverse Kubernetes resources and view the state of your clusters in a single powerful session.