k9s/internal/config/views.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
}