k9s/internal/config/views.go

172 lines
3.7 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/config/data"
"github.com/derailed/k9s/internal/config/json"
"github.com/derailed/k9s/internal/slogs"
"gopkg.in/yaml.v2"
)
// 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() (string, bool, 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
}
// AddListener registers a new listener.
func (v *CustomView) AddListener(gvr string, l ViewConfigListener) {
v.listeners[gvr] = l
v.fireConfigChanged()
}
// RemoveListener unregister a listener.
func (v *CustomView) RemoveListener(gvr string) {
delete(v.listeners, gvr)
}
func (v *CustomView) fireConfigChanged() {
for gvr, list := range v.listeners {
if vs := v.getVS(gvr, list.GetNamespace()); vs == nil {
list.ViewSettingsChanged(nil)
} else {
slog.Debug("Reloading custom view settings", slogs.GVR, gvr)
list.ViewSettingsChanged(vs)
}
}
}
func (v *CustomView) getVS(gvr, ns string) *ViewSetting {
k := gvr
if ns != "" {
k += "@" + ns
}
for key := range maps.Keys(v.Views) {
if !strings.HasPrefix(key, gvr) {
continue
}
switch {
case key == k:
vs := v.Views[key]
return &vs
case strings.Contains(key, "@"):
tt := strings.Split(key, "@")
if len(tt) != 2 {
break
}
if rx, err := regexp.Compile(tt[1]); err == nil && rx.MatchString(k) {
vs := v.Views[key]
return &vs
}
}
}
return nil
}