224 lines
4.9 KiB
Go
224 lines
4.9 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright Authors of K9s
|
|
|
|
package config
|
|
|
|
import (
|
|
"cmp"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"log/slog"
|
|
"maps"
|
|
"os"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/derailed/k9s/internal/client"
|
|
"github.com/derailed/k9s/internal/config/data"
|
|
"github.com/derailed/k9s/internal/config/json"
|
|
"github.com/derailed/k9s/internal/slogs"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// ViewConfigListener represents a view config listener.
|
|
type ViewConfigListener interface {
|
|
// ViewSettingsChanged notifies listener the view configuration changed.
|
|
ViewSettingsChanged(*ViewSetting)
|
|
|
|
// GetNamespace return the view namespace
|
|
GetNamespace() string
|
|
}
|
|
|
|
// ViewSetting represents a view configuration.
|
|
type ViewSetting struct {
|
|
Columns []string `yaml:"columns"`
|
|
SortColumn string `yaml:"sortColumn"`
|
|
}
|
|
|
|
func (v *ViewSetting) HasCols() bool {
|
|
return len(v.Columns) > 0
|
|
}
|
|
|
|
func (v *ViewSetting) IsBlank() bool {
|
|
return v == nil || (len(v.Columns) == 0 && v.SortColumn == "")
|
|
}
|
|
|
|
func (v *ViewSetting) SortCol() (name string, asc bool, err error) {
|
|
if v == nil || v.SortColumn == "" {
|
|
return "", false, fmt.Errorf("no sort column specified")
|
|
}
|
|
tt := strings.Split(v.SortColumn, ":")
|
|
if len(tt) < 2 {
|
|
return "", false, fmt.Errorf("invalid sort column spec: %q. must be col-name:asc|desc", v.SortColumn)
|
|
}
|
|
|
|
return tt[0], tt[1] == "asc", nil
|
|
}
|
|
|
|
// Equals checks if two view settings are equal.
|
|
func (v *ViewSetting) Equals(vs *ViewSetting) bool {
|
|
if v == nil && vs == nil {
|
|
return true
|
|
}
|
|
if v == nil || vs == nil {
|
|
return false
|
|
}
|
|
|
|
if c := slices.Compare(v.Columns, vs.Columns); c != 0 {
|
|
return false
|
|
}
|
|
|
|
return cmp.Compare(v.SortColumn, vs.SortColumn) == 0
|
|
}
|
|
|
|
// CustomView represents a collection of view customization.
|
|
type CustomView struct {
|
|
Views map[string]ViewSetting `yaml:"views"`
|
|
listeners map[string]ViewConfigListener
|
|
}
|
|
|
|
// NewCustomView returns a views configuration.
|
|
func NewCustomView() *CustomView {
|
|
return &CustomView{
|
|
Views: make(map[string]ViewSetting),
|
|
listeners: make(map[string]ViewConfigListener),
|
|
}
|
|
}
|
|
|
|
// Reset clears out configurations.
|
|
func (v *CustomView) Reset() {
|
|
for k := range v.Views {
|
|
delete(v.Views, k)
|
|
}
|
|
}
|
|
|
|
// Load loads view configurations.
|
|
func (v *CustomView) Load(path string) error {
|
|
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
|
|
return nil
|
|
}
|
|
bb, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := data.JSONValidator.Validate(json.ViewsSchema, bb); err != nil {
|
|
slog.Warn("Validation failed. Please update your config and restart!",
|
|
slogs.Path, path,
|
|
slogs.Error, err,
|
|
)
|
|
}
|
|
var in CustomView
|
|
if err := yaml.Unmarshal(bb, &in); err != nil {
|
|
return err
|
|
}
|
|
v.Views = in.Views
|
|
v.fireConfigChanged()
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddListeners registers a new listener for various commands.
|
|
func (v *CustomView) AddListeners(l ViewConfigListener, cmds ...string) {
|
|
for _, cmd := range cmds {
|
|
if cmd != "" {
|
|
v.listeners[cmd] = l
|
|
}
|
|
}
|
|
v.fireConfigChanged()
|
|
}
|
|
|
|
// AddListener registers a new listener.
|
|
func (v *CustomView) AddListener(cmd string, l ViewConfigListener) {
|
|
v.listeners[cmd] = l
|
|
v.fireConfigChanged()
|
|
}
|
|
|
|
// RemoveListener unregister a listener.
|
|
func (v *CustomView) RemoveListener(l ViewConfigListener) {
|
|
for k, list := range v.listeners {
|
|
if list == l {
|
|
delete(v.listeners, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *CustomView) fireConfigChanged() {
|
|
cmds := slices.Collect(maps.Keys(v.listeners))
|
|
slices.SortFunc(cmds, func(a, b string) int {
|
|
switch {
|
|
case strings.Contains(a, "/") && !strings.Contains(b, "/"):
|
|
return 1
|
|
case !strings.Contains(a, "/") && strings.Contains(b, "/"):
|
|
return -1
|
|
default:
|
|
return strings.Compare(a, b)
|
|
}
|
|
})
|
|
type tuple struct {
|
|
cmd string
|
|
vs *ViewSetting
|
|
}
|
|
var victim tuple
|
|
for _, cmd := range cmds {
|
|
if vs := v.getVS(cmd, v.listeners[cmd].GetNamespace()); vs != nil {
|
|
slog.Debug("Reloading custom view settings", slogs.Command, cmd)
|
|
victim = tuple{cmd, vs}
|
|
break
|
|
}
|
|
victim = tuple{cmd, nil}
|
|
}
|
|
if victim.cmd != "" {
|
|
v.listeners[victim.cmd].ViewSettingsChanged(victim.vs)
|
|
}
|
|
}
|
|
|
|
func (v *CustomView) getVS(gvr, ns string) *ViewSetting {
|
|
if client.IsAllNamespaces(ns) {
|
|
ns = client.NamespaceAll
|
|
}
|
|
k := gvr
|
|
kk := slices.Collect(maps.Keys(v.Views))
|
|
slices.SortFunc(kk, strings.Compare)
|
|
slices.Reverse(kk)
|
|
for _, key := range kk {
|
|
if !strings.HasPrefix(key, gvr) && !strings.HasPrefix(gvr, key) {
|
|
continue
|
|
}
|
|
|
|
switch {
|
|
case strings.Contains(key, "@"):
|
|
tt := strings.Split(key, "@")
|
|
if len(tt) != 2 {
|
|
break
|
|
}
|
|
nsk := gvr
|
|
if ns != "" {
|
|
nsk += "@" + ns
|
|
}
|
|
if rx, err := regexp.Compile(tt[1]); err == nil && rx.MatchString(nsk) {
|
|
vs := v.Views[key]
|
|
return &vs
|
|
}
|
|
case strings.HasPrefix(k, key):
|
|
kk := strings.Fields(k)
|
|
if len(kk) == 2 {
|
|
if v, ok := v.Views[kk[0]+"@"+kk[1]]; ok {
|
|
return &v
|
|
}
|
|
if key == kk[0] {
|
|
vs := v.Views[key]
|
|
return &vs
|
|
}
|
|
}
|
|
fallthrough
|
|
case key == k:
|
|
vs := v.Views[key]
|
|
return &vs
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|