Rel v0.40.9 (#3205)
* update write icon * fix#3202 * rel docs * update linter * lint * test * lintmine
parent
39eef03373
commit
fc5f1907c4
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
cache-dependency-path: go.sum
|
||||
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v6.5.0
|
||||
uses: golangci/golangci-lint-action@v6.5.1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
reporter: github-pr-check
|
||||
|
|
@ -50,7 +50,12 @@ linters-settings:
|
|||
goimports:
|
||||
local-prefixes: github.com/cilium/cilium
|
||||
unused:
|
||||
go: "1.23"
|
||||
parameters-are-used: true
|
||||
local-variables-are-used: true
|
||||
field-writes-are-uses: true
|
||||
post-statements-are-reads: true
|
||||
exported-fields-are-used: true
|
||||
generated-is-used: true
|
||||
goheader:
|
||||
values:
|
||||
regexp:
|
||||
|
|
@ -61,23 +66,6 @@ linters-settings:
|
|||
gosec:
|
||||
includes:
|
||||
- G402
|
||||
gomodguard:
|
||||
blocked:
|
||||
modules:
|
||||
- github.com/miekg/dns:
|
||||
recommendations:
|
||||
- github.com/cilium/dns
|
||||
reason: "use the cilium fork directly to avoid replace directives in go.mod, see https://github.com/cilium/cilium/pull/27582"
|
||||
- gopkg.in/check.v1:
|
||||
recommendations:
|
||||
- testing
|
||||
- github.com/stretchr/testify/assert
|
||||
reason: "gocheck has been deprecated, see https://docs.cilium.io/en/latest/contributing/testing/unit/#migrating-tests-off-of-gopkg-in-check-v1"
|
||||
- go.uber.org/multierr:
|
||||
recommendations:
|
||||
- errors
|
||||
reason: "Go 1.20+ has support for combining multiple errors, see https://go.dev/doc/go1.20#errors"
|
||||
|
||||
sloglint:
|
||||
# Enforce not mixing key-value pairs and attributes.
|
||||
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-mixed-arguments
|
||||
|
|
@ -138,7 +126,7 @@ issues:
|
|||
|
||||
# default is true. Enables skipping of directories:
|
||||
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
|
||||
skip-dirs-use-default: true
|
||||
exclude-dirs-use-default: true
|
||||
|
||||
# Excluding configuration per-path, per-linter, per-text and per-source
|
||||
exclude-rules:
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:
|
|||
else
|
||||
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
|
||||
endif
|
||||
VERSION ?= v0.40.8
|
||||
VERSION ?= v0.40.9
|
||||
IMG_NAME := derailed/k9s
|
||||
IMAGE := ${IMG_NAME}:${VERSION}
|
||||
|
||||
|
|
|
|||
54
README.md
54
README.md
|
|
@ -726,7 +726,15 @@ views:
|
|||
|
||||
## Plugins
|
||||
|
||||
K9s allows you to extend your command line and tooling by defining your very own cluster commands via plugins. K9s will look at `$XDG_CONFIG_HOME/k9s/plugins.yaml` to locate all available plugins.
|
||||
K9s allows you to extend your command line and tooling by defining your very own cluster commands via plugins.
|
||||
Minimally we look at `$XDG_CONFIG_HOME/k9s/plugins.yaml` to locate all available plugins.
|
||||
Additionally, K9s will scan the following directories for additional plugins:
|
||||
|
||||
* `$XDG_CONFIG_HOME/k9s/plugins`
|
||||
* `$XDG_DATA_HOME/k9s/plugins`
|
||||
* `$XDG_DATA_DIRS/k9s/plugins`
|
||||
|
||||
The plugin file content can be either a single plugin snippet, a collections of snippets or a complete plugins definition (see examples below...).
|
||||
|
||||
A plugin is defined as follows:
|
||||
|
||||
|
|
@ -760,12 +768,15 @@ K9s does provide additional environment variables for you to customize your plug
|
|||
|
||||
Curly braces can be used to embed an environment variable inside another string, or if the column name contains special characters. (e.g. `${NAME}-example` or `${COL-%CPU/L}`)
|
||||
|
||||
### Plugin Example
|
||||
### Plugin Examples
|
||||
|
||||
This defines a plugin for viewing logs on a selected pod using `ctrl-l` as shortcut.
|
||||
Define several plugins and host them in a single file. These can leave in the K9s root config so that they are available on any clusters. Additionally, you can define cluster/context specific plugins for your clusters of choice by adding clusterA/contextB/plugins.yaml file.
|
||||
|
||||
The following defines a plugin for viewing logs on a selected pod using `ctrl-l` as shortcut.
|
||||
|
||||
```yaml
|
||||
# $XDG_DATA_HOME/k9s/plugins.yaml
|
||||
# Define several plugins in a single file in the K9s root configuration
|
||||
# $XDG_DATA_HOME/k9s/plugins.yaml
|
||||
plugins:
|
||||
# Defines a plugin to provide a `ctrl-l` shortcut to tail the logs while in pod view.
|
||||
fred:
|
||||
|
|
@ -789,6 +800,41 @@ plugins:
|
|||
- $CONTEXT
|
||||
```
|
||||
|
||||
Similarly you can define the plugin above in a directory using either a file per plugin or several plugins per files as follow...
|
||||
|
||||
The following defines two plugins namely fred and zorg.
|
||||
|
||||
```yaml
|
||||
# Multiple plugins in a single file...
|
||||
# Note: as of v0.40.9 you can have ad-hoc plugin dirs
|
||||
# Loads plugins fred and zorg
|
||||
# $XDG_DATA_HOME/k9s/plugins/misc-plugins/blee.yaml
|
||||
fred:
|
||||
shortCut: Shift-B
|
||||
description: Bozo
|
||||
scopes:
|
||||
- deploy
|
||||
command: bozo
|
||||
|
||||
zorg:
|
||||
shortCut: Shift-Z
|
||||
description: Pod logs
|
||||
scopes:
|
||||
- svc
|
||||
command: zorg
|
||||
```
|
||||
|
||||
Lastly you can define plugin snippets in their own file. The snippet will be named from the file name. In this case, we define a `bozo` plugin using a plugin snippet.
|
||||
|
||||
```yaml
|
||||
# $XDG_DATA_HOME/k9s/plugins/schtuff/bozo.yaml
|
||||
shortCut: Shift-B
|
||||
description: Bozo
|
||||
scopes:
|
||||
- deploy
|
||||
command: bozo
|
||||
```
|
||||
|
||||
> NOTE: This is an experimental feature! Options and layout may change in future K9s releases as this feature solidifies.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
|
||||
|
||||
# Release v0.40.9
|
||||
|
||||
## 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)
|
||||
|
||||
## Maintenance Release!
|
||||
|
||||
* Refactored plugins implementation, hopefully we didn't hose them 😳
|
||||
* Updated plugins docs
|
||||
* Apparently when it comes to icons, I've chosen... poorly 🙀
|
||||
Updated `write` icon 🔓->✍️, hopefully for the better 👀??
|
||||
|
||||
## Videos Are In The Can!
|
||||
|
||||
Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...
|
||||
|
||||
* [K9s v0.40.0 -Column 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
|
||||
|
||||
* [#3202](https://github.com/derailed/k9s/issues/3202) 0.40.8 breaks plugins loading
|
||||
|
||||
---
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
|
@ -28,6 +28,7 @@ func isStringSet(s *string) bool {
|
|||
|
||||
func isYamlFile(file string) bool {
|
||||
ext := filepath.Ext(file)
|
||||
|
||||
return ext == ".yml" || ext == ".yaml"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "K9s plugin-multi schema",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "file://internal/config/json/schemas/plugin.json",
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "K9s plugin schema",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"shortCut": { "type": "string" },
|
||||
"override": { "type": "boolean" },
|
||||
"description": { "type": "string" },
|
||||
"confirm": { "type": "boolean" },
|
||||
"dangerous": { "type": "boolean" },
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"command": { "type": "string" },
|
||||
"background": { "type": "boolean" },
|
||||
"overwriteOutput": { "type": "boolean" },
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": { "type": ["string", "number"] }
|
||||
}
|
||||
},
|
||||
"required": ["shortCut", "description", "scopes", "command"]
|
||||
}
|
||||
|
|
@ -7,27 +7,8 @@
|
|||
"plugins": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"shortCut": { "type": "string" },
|
||||
"override": { "type": "boolean" },
|
||||
"description": { "type": "string" },
|
||||
"confirm": { "type": "boolean" },
|
||||
"dangerous": { "type": "boolean" },
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"command": { "type": "string" },
|
||||
"background": { "type": "boolean" },
|
||||
"overwriteOutput": { "type": "boolean" },
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": { "type": ["string", "number"] }
|
||||
}
|
||||
},
|
||||
"required": ["shortCut", "description", "scopes", "command"]
|
||||
"$ref": "file://internal/config/json/schemas/plugin.json",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
shortCut: g
|
||||
confirm: false
|
||||
description: blee
|
||||
scopes:
|
||||
- namespaces
|
||||
command: sh
|
||||
background: false
|
||||
args:
|
||||
- -c
|
||||
- "blee bla"
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
blee:
|
||||
shortCut: g
|
||||
confirm: false
|
||||
description: blee
|
||||
scopes:
|
||||
- namespaces
|
||||
command: sh
|
||||
background: false
|
||||
args:
|
||||
- -c
|
||||
- "blee bla"
|
||||
|
||||
duh:
|
||||
shortCut: h
|
||||
confirm: true
|
||||
description: duh
|
||||
scopes:
|
||||
- all
|
||||
command: sh
|
||||
background: true
|
||||
args:
|
||||
- -c
|
||||
- "duh fred"
|
||||
|
|
@ -20,6 +20,12 @@ const (
|
|||
// PluginsSchema describes plugins schema.
|
||||
PluginsSchema = "plugins.json"
|
||||
|
||||
// PluginSchema describes a plugin snippet schema.
|
||||
PluginSchema = "plugin.json"
|
||||
|
||||
// PluginMultiSchema describes plugin snippets schema.
|
||||
PluginMultiSchema = "plugin-multi.json"
|
||||
|
||||
// AliasesSchema describes aliases schema.
|
||||
AliasesSchema = "aliases.json"
|
||||
|
||||
|
|
@ -41,8 +47,14 @@ const (
|
|||
|
||||
var (
|
||||
//go:embed schemas/plugins.json
|
||||
pluginsSchema string
|
||||
|
||||
//go:embed schemas/plugin.json
|
||||
pluginSchema string
|
||||
|
||||
//go:embed schemas/plugin-multi.json
|
||||
pluginMultiSchema string
|
||||
|
||||
//go:embed schemas/aliases.json
|
||||
aliasSchema string
|
||||
|
||||
|
|
@ -72,13 +84,15 @@ type Validator struct {
|
|||
func NewValidator() *Validator {
|
||||
v := Validator{
|
||||
schemas: map[string]gojsonschema.JSONLoader{
|
||||
K9sSchema: gojsonschema.NewStringLoader(k9sSchema),
|
||||
ContextSchema: gojsonschema.NewStringLoader(contextSchema),
|
||||
AliasesSchema: gojsonschema.NewStringLoader(aliasSchema),
|
||||
ViewsSchema: gojsonschema.NewStringLoader(viewsSchema),
|
||||
PluginsSchema: gojsonschema.NewStringLoader(pluginSchema),
|
||||
HotkeysSchema: gojsonschema.NewStringLoader(hotkeysSchema),
|
||||
SkinSchema: gojsonschema.NewStringLoader(skinSchema),
|
||||
K9sSchema: gojsonschema.NewStringLoader(k9sSchema),
|
||||
ContextSchema: gojsonschema.NewStringLoader(contextSchema),
|
||||
AliasesSchema: gojsonschema.NewStringLoader(aliasSchema),
|
||||
ViewsSchema: gojsonschema.NewStringLoader(viewsSchema),
|
||||
PluginsSchema: gojsonschema.NewStringLoader(pluginsSchema),
|
||||
PluginSchema: gojsonschema.NewStringLoader(pluginSchema),
|
||||
PluginMultiSchema: gojsonschema.NewStringLoader(pluginMultiSchema),
|
||||
HotkeysSchema: gojsonschema.NewStringLoader(hotkeysSchema),
|
||||
SkinSchema: gojsonschema.NewStringLoader(skinSchema),
|
||||
},
|
||||
}
|
||||
v.register()
|
||||
|
|
@ -92,7 +106,6 @@ func (v *Validator) register() {
|
|||
v.loader.Validate = true
|
||||
|
||||
clog := slog.With(slogs.Subsys, "schema")
|
||||
|
||||
for k, s := range v.schemas {
|
||||
if err := v.loader.AddSchema(k, s); err != nil {
|
||||
clog.Error("Schema initialization failed",
|
||||
|
|
@ -103,9 +116,24 @@ func (v *Validator) register() {
|
|||
}
|
||||
}
|
||||
|
||||
// ValidatePlugins validates plugins schema.
|
||||
// Checks for full, snippet and multi snippets schemas.
|
||||
func (v *Validator) ValidatePlugins(bb []byte) (string, error) {
|
||||
var errs error
|
||||
for _, k := range []string{PluginsSchema, PluginSchema, PluginMultiSchema} {
|
||||
if err := v.Validate(k, bb); err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
continue
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
return "", errs
|
||||
}
|
||||
|
||||
// Validate runs document thru given schema validation.
|
||||
func (v *Validator) Validate(k string, bb []byte) error {
|
||||
var m interface{}
|
||||
var m any
|
||||
err := yaml.Unmarshal(bb, &m)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -14,11 +14,59 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidatePluginDir(t *testing.T) {
|
||||
skinDir := "../../../plugins"
|
||||
ee, err := os.ReadDir(skinDir)
|
||||
func TestValidatePluginSnippet(t *testing.T) {
|
||||
plugPath := "testdata/plugins/snippet.yaml"
|
||||
bb, err := os.ReadFile(plugPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
p := json.NewValidator()
|
||||
assert.NoError(t, p.Validate(json.PluginSchema, bb), plugPath)
|
||||
}
|
||||
|
||||
func TestValidatePlugins(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
path, schema string
|
||||
err string
|
||||
}{
|
||||
"cool": {
|
||||
path: "testdata/plugins/cool.yaml",
|
||||
schema: json.PluginsSchema,
|
||||
},
|
||||
"toast": {
|
||||
path: "testdata/plugins/toast.yaml",
|
||||
schema: json.PluginsSchema,
|
||||
err: "Additional property shortCuts is not allowed\nscopes is required\nshortCut is required",
|
||||
},
|
||||
"cool-snippet": {
|
||||
path: "testdata/plugins/snippet.yaml",
|
||||
schema: json.PluginSchema,
|
||||
},
|
||||
"cool-snippets": {
|
||||
path: "testdata/plugins/snippets.yaml",
|
||||
schema: json.PluginMultiSchema,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
bb, err := os.ReadFile(u.path)
|
||||
assert.NoError(t, err)
|
||||
dir, _ := os.Getwd()
|
||||
assert.NoError(t, os.Chdir("../../.."))
|
||||
v := json.NewValidator()
|
||||
if err := v.Validate(u.schema, bb); err != nil {
|
||||
assert.Equal(t, u.err, err.Error())
|
||||
}
|
||||
assert.NoError(t, os.Chdir(dir))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePluginDir(t *testing.T) {
|
||||
plugDir := "../../../plugins"
|
||||
ee, err := os.ReadDir(plugDir)
|
||||
assert.NoError(t, err)
|
||||
for _, e := range ee {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
|
|
@ -28,10 +76,15 @@ func TestValidatePluginDir(t *testing.T) {
|
|||
continue
|
||||
}
|
||||
assert.True(t, ext == ".yaml", fmt.Sprintf("expected yaml file: %q", e.Name()))
|
||||
assert.True(t, !strings.Contains(e.Name(), "_"), fmt.Sprintf("underscore in: %q", e.Name()))
|
||||
bb, err := os.ReadFile(filepath.Join(skinDir, e.Name()))
|
||||
assert.False(t, strings.Contains(e.Name(), "_"), fmt.Sprintf("underscore in: %q", e.Name()))
|
||||
bb, err := os.ReadFile(filepath.Join(plugDir, e.Name()))
|
||||
assert.NoError(t, err)
|
||||
|
||||
dir, _ := os.Getwd()
|
||||
assert.NoError(t, os.Chdir("../../.."))
|
||||
p := json.NewValidator()
|
||||
assert.NoError(t, p.Validate(json.PluginsSchema, bb), e.Name())
|
||||
assert.NoError(t, os.Chdir(dir))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,35 +188,6 @@ Additional property namespaces is not allowed`,
|
|||
}
|
||||
}
|
||||
|
||||
func TestValidatePlugins(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
f string
|
||||
err string
|
||||
}{
|
||||
"happy": {
|
||||
f: "testdata/plugins/cool.yaml",
|
||||
},
|
||||
"toast": {
|
||||
f: "testdata/plugins/toast.yaml",
|
||||
err: `Additional property shortCuts is not allowed
|
||||
scopes is required
|
||||
shortCut is required`,
|
||||
},
|
||||
}
|
||||
|
||||
v := json.NewValidator()
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
bb, err := os.ReadFile(u.f)
|
||||
assert.NoError(t, err)
|
||||
if err := v.Validate(json.PluginsSchema, bb); err != nil {
|
||||
assert.Equal(t, u.err, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAliases(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
f string
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ import (
|
|||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const k9sPluginsDir = "k9s/plugins"
|
||||
type plugins map[string]Plugin
|
||||
|
||||
// Plugins represents a collection of plugins.
|
||||
type Plugins struct {
|
||||
Plugins map[string]Plugin `yaml:"plugins"`
|
||||
Plugins plugins `yaml:"plugins"`
|
||||
}
|
||||
|
||||
// Plugin describes a K9s plugin.
|
||||
|
|
@ -54,18 +54,27 @@ func NewPlugins() Plugins {
|
|||
}
|
||||
|
||||
// Load K9s plugins.
|
||||
func (p Plugins) Load(path string) error {
|
||||
func (p Plugins) Load(path string, loadExtra bool) error {
|
||||
var errs error
|
||||
|
||||
// Load from global config file
|
||||
if err := p.load(AppPluginsFile); err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
// Load from cluster/context config
|
||||
if err := p.load(path); err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
for _, dataDir := range append(xdg.DataDirs, xdg.DataHome, xdg.ConfigHome) {
|
||||
if err := p.loadPluginDir(filepath.Join(dataDir, k9sPluginsDir)); err != nil {
|
||||
if !loadExtra {
|
||||
return errs
|
||||
}
|
||||
// Load from XDG dirs
|
||||
const k9sPluginsDir = "k9s/plugins"
|
||||
for _, dir := range append(xdg.DataDirs, xdg.DataHome, xdg.ConfigHome) {
|
||||
path := filepath.Join(dir, k9sPluginsDir)
|
||||
if err := p.loadDir(path); err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -73,43 +82,6 @@ func (p Plugins) Load(path string) error {
|
|||
return errs
|
||||
}
|
||||
|
||||
func (p Plugins) loadPluginDir(dir string) error {
|
||||
pluginFiles, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs error
|
||||
for _, file := range pluginFiles {
|
||||
if file.IsDir() || !isYamlFile(file.Name()) {
|
||||
continue
|
||||
}
|
||||
fileName := filepath.Join(dir, file.Name())
|
||||
fileContent, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
d := yaml.NewDecoder(bytes.NewReader(fileContent))
|
||||
d.KnownFields(true)
|
||||
|
||||
var plugin Plugin
|
||||
if err = d.Decode(&plugin); err != nil {
|
||||
var plugins Plugins
|
||||
if err = d.Decode(&plugins); err != nil {
|
||||
return fmt.Errorf("cannot parse %s into either a single plugin nor plugins: %w", fileName, err)
|
||||
}
|
||||
for name, plugin := range plugins.Plugins {
|
||||
p.Plugins[name] = plugin
|
||||
}
|
||||
continue
|
||||
}
|
||||
p.Plugins[strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))] = plugin
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (p *Plugins) load(path string) error {
|
||||
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
|
|
@ -118,19 +90,62 @@ func (p *Plugins) load(path string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := data.JSONValidator.Validate(json.PluginsSchema, bb); err != nil {
|
||||
slog.Warn("Validation failed. Please update your config and restart!",
|
||||
scheme, err := data.JSONValidator.ValidatePlugins(bb)
|
||||
if err != nil {
|
||||
slog.Warn("Plugin schema validation failed",
|
||||
slogs.Path, path,
|
||||
slogs.Error, err,
|
||||
)
|
||||
return fmt.Errorf("plugin validation failed for %s: %w", path, err)
|
||||
}
|
||||
var pp Plugins
|
||||
if err := yaml.Unmarshal(bb, &pp); err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range pp.Plugins {
|
||||
p.Plugins[k] = v
|
||||
|
||||
d := yaml.NewDecoder(bytes.NewReader(bb))
|
||||
d.KnownFields(true)
|
||||
|
||||
switch scheme {
|
||||
case json.PluginSchema:
|
||||
var o Plugin
|
||||
if err := yaml.Unmarshal(bb, &o); err != nil {
|
||||
return fmt.Errorf("plugin unmarshal failed for %s: %w", path, err)
|
||||
}
|
||||
p.Plugins[strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))] = o
|
||||
case json.PluginsSchema:
|
||||
var oo Plugins
|
||||
if err := yaml.Unmarshal(bb, &oo); err != nil {
|
||||
return fmt.Errorf("plugin unmarshal failed for %s: %w", path, err)
|
||||
}
|
||||
for k, v := range oo.Plugins {
|
||||
p.Plugins[k] = v
|
||||
}
|
||||
case json.PluginMultiSchema:
|
||||
var oo plugins
|
||||
if err := yaml.Unmarshal(bb, &oo); err != nil {
|
||||
return fmt.Errorf("plugin unmarshal failed for %s: %w", path, err)
|
||||
}
|
||||
for k, v := range oo {
|
||||
p.Plugins[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Plugins) loadDir(dir string) error {
|
||||
if _, err := os.Stat(dir); errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs error
|
||||
errs = errors.Join(errs, filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() || !isYamlFile(info.Name()) {
|
||||
return nil
|
||||
}
|
||||
errs = errors.Join(errs, p.load(path))
|
||||
return nil
|
||||
}))
|
||||
|
||||
return errs
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,85 +5,193 @@ package config
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var pluginYmlTestData = Plugin{
|
||||
Scopes: []string{"po", "dp"},
|
||||
Args: []string{"-n", "$NAMESPACE", "-boolean"},
|
||||
ShortCut: "shift-s",
|
||||
Description: "blee",
|
||||
Command: "duh",
|
||||
Confirm: true,
|
||||
Background: false,
|
||||
}
|
||||
|
||||
var test1YmlTestData = Plugin{
|
||||
Scopes: []string{"po", "dp"},
|
||||
Args: []string{"-n", "$NAMESPACE", "-boolean"},
|
||||
ShortCut: "shift-s",
|
||||
Description: "blee",
|
||||
Command: "duh",
|
||||
Confirm: true,
|
||||
Background: false,
|
||||
OverwriteOutput: true,
|
||||
}
|
||||
|
||||
var test2YmlTestData = Plugin{
|
||||
Scopes: []string{"svc", "ing"},
|
||||
Args: []string{"-n", "$NAMESPACE", "-oyaml"},
|
||||
ShortCut: "shift-r",
|
||||
Description: "bla",
|
||||
Command: "duha",
|
||||
Confirm: false,
|
||||
Background: true,
|
||||
OverwriteOutput: false,
|
||||
}
|
||||
|
||||
func TestPluginLoad(t *testing.T) {
|
||||
AppPluginsFile = "/tmp/k9s-test/fred.yaml"
|
||||
os.Setenv("XDG_DATA_HOME", "/tmp/k9s-test")
|
||||
xdg.Reload()
|
||||
uu := map[string]struct {
|
||||
path string
|
||||
err string
|
||||
ee Plugins
|
||||
}{
|
||||
"snippet": {
|
||||
path: "testdata/plugins/dir/snippet.1.yaml",
|
||||
ee: Plugins{
|
||||
Plugins: plugins{
|
||||
"snippet.1": Plugin{
|
||||
Scopes: []string{"po", "dp"},
|
||||
Args: []string{"-n", "$NAMESPACE", "-boolean"},
|
||||
ShortCut: "shift-s",
|
||||
Description: "blee",
|
||||
Command: "duh",
|
||||
Confirm: true,
|
||||
OverwriteOutput: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
p := NewPlugins()
|
||||
assert.NoError(t, p.Load("testdata/plugins.yaml"))
|
||||
"multi-snippets": {
|
||||
path: "testdata/plugins/dir/snippet.multi.yaml",
|
||||
ee: Plugins{
|
||||
Plugins: plugins{
|
||||
"crapola": Plugin{
|
||||
ShortCut: "Shift-1",
|
||||
Command: "crapola",
|
||||
Description: "crapola",
|
||||
Scopes: []string{"pods"},
|
||||
},
|
||||
"bozo": Plugin{
|
||||
ShortCut: "Shift-2",
|
||||
Description: "bozo",
|
||||
Command: "bozo",
|
||||
Scopes: []string{"pods", "svc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
assert.Equal(t, 1, len(p.Plugins))
|
||||
k, ok := p.Plugins["blah"]
|
||||
assert.True(t, ok)
|
||||
assert.ObjectsAreEqual(pluginYmlTestData, k)
|
||||
"full": {
|
||||
path: "testdata/plugins/plugins.yaml",
|
||||
ee: Plugins{
|
||||
Plugins: plugins{
|
||||
"blah": Plugin{
|
||||
Scopes: []string{"po", "dp"},
|
||||
Args: []string{"-n", "$NAMESPACE", "-boolean"},
|
||||
ShortCut: "shift-s",
|
||||
Description: "blee",
|
||||
Command: "duh",
|
||||
Confirm: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"toast-no-file": {
|
||||
path: "testdata/plugins/plugins-bozo.yaml",
|
||||
ee: NewPlugins(),
|
||||
},
|
||||
|
||||
"toast-invalid": {
|
||||
path: "testdata/plugins/plugins-toast.yaml",
|
||||
ee: NewPlugins(),
|
||||
err: "Additional property scoped is not allowed\nscopes is required\nAdditional property plugins is not allowed\ncommand is required\ndescription is required\nscopes is required\nshortCut is required\nAdditional property blah is not allowed\ncommand is required\ndescription is required\nscopes is required\nshortCut is required",
|
||||
},
|
||||
}
|
||||
|
||||
dir, _ := os.Getwd()
|
||||
assert.NoError(t, os.Chdir("../.."))
|
||||
defer func() {
|
||||
assert.NoError(t, os.Chdir(dir))
|
||||
}()
|
||||
for k, u := range uu {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
p := NewPlugins()
|
||||
err := p.Load(path.Join(dir, u.path), false)
|
||||
if err != nil {
|
||||
idx := strings.Index(err.Error(), ":")
|
||||
assert.Equal(t, u.err, err.Error()[idx+2:])
|
||||
}
|
||||
assert.Equal(t, u.ee, p)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSinglePluginFileLoad(t *testing.T) {
|
||||
e := Plugin{
|
||||
Scopes: []string{"po", "dp"},
|
||||
Args: []string{"-n", "$NAMESPACE", "-boolean"},
|
||||
ShortCut: "shift-s",
|
||||
Description: "blee",
|
||||
Command: "duh",
|
||||
Confirm: true,
|
||||
}
|
||||
|
||||
dir, _ := os.Getwd()
|
||||
assert.NoError(t, os.Chdir("../.."))
|
||||
defer func() {
|
||||
assert.NoError(t, os.Chdir(dir))
|
||||
}()
|
||||
|
||||
p := NewPlugins()
|
||||
assert.NoError(t, p.load("testdata/plugins.yaml"))
|
||||
assert.NoError(t, p.loadPluginDir("/random/dir/not/exist"))
|
||||
assert.NoError(t, p.load(path.Join(dir, "testdata/plugins/plugins.yaml")))
|
||||
assert.NoError(t, p.loadDir(path.Join(dir, "/random/dir/not/exist")))
|
||||
|
||||
assert.Equal(t, 1, len(p.Plugins))
|
||||
k, ok := p.Plugins["blah"]
|
||||
assert.True(t, ok)
|
||||
v, ok := p.Plugins["blah"]
|
||||
|
||||
assert.ObjectsAreEqual(pluginYmlTestData, k)
|
||||
assert.True(t, ok)
|
||||
assert.ObjectsAreEqual(e, v)
|
||||
}
|
||||
|
||||
func TestMultiplePluginFilesLoad(t *testing.T) {
|
||||
p := NewPlugins()
|
||||
assert.NoError(t, p.load("testdata/plugins.yaml"))
|
||||
assert.NoError(t, p.loadPluginDir("testdata/plugins"))
|
||||
|
||||
testPlugins := map[string]Plugin{
|
||||
"blah": pluginYmlTestData,
|
||||
"test1": test1YmlTestData,
|
||||
"test2": test2YmlTestData,
|
||||
uu := map[string]struct {
|
||||
path string
|
||||
dir string
|
||||
ee Plugins
|
||||
}{
|
||||
"empty": {
|
||||
path: "internal/config/testdata/plugins/plugins.yaml",
|
||||
dir: "internal/config/testdata/plugins/dir",
|
||||
ee: Plugins{
|
||||
Plugins: plugins{
|
||||
"blah": {
|
||||
Scopes: []string{"po", "dp"},
|
||||
Args: []string{"-n", "$NAMESPACE", "-boolean"},
|
||||
ShortCut: "shift-s",
|
||||
Description: "blee",
|
||||
Command: "duh",
|
||||
Confirm: true,
|
||||
},
|
||||
"snippet.1": {
|
||||
ShortCut: "shift-s",
|
||||
Command: "duh",
|
||||
Scopes: []string{"po", "dp"},
|
||||
Args: []string{"-n", "$NAMESPACE", "-boolean"},
|
||||
Description: "blee",
|
||||
Confirm: true,
|
||||
OverwriteOutput: true,
|
||||
},
|
||||
"snippet.2": {
|
||||
Scopes: []string{"svc", "ing"},
|
||||
Args: []string{"-n", "$NAMESPACE", "-oyaml"},
|
||||
ShortCut: "shift-r",
|
||||
Description: "bla",
|
||||
Command: "duha",
|
||||
Background: true,
|
||||
},
|
||||
"crapola": {
|
||||
Scopes: []string{"pods"},
|
||||
Command: "crapola",
|
||||
Description: "crapola",
|
||||
ShortCut: "Shift-1",
|
||||
},
|
||||
"bozo": {
|
||||
Scopes: []string{"pods", "svc"},
|
||||
Command: "bozo",
|
||||
Description: "bozo",
|
||||
ShortCut: "Shift-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, len(testPlugins), len(p.Plugins))
|
||||
for name, expectedPlugin := range testPlugins {
|
||||
k, ok := p.Plugins[name]
|
||||
assert.True(t, ok)
|
||||
assert.ObjectsAreEqual(expectedPlugin, k)
|
||||
dir, _ := os.Getwd()
|
||||
assert.NoError(t, os.Chdir("../.."))
|
||||
defer func() {
|
||||
assert.NoError(t, os.Chdir(dir))
|
||||
}()
|
||||
for k, u := range uu {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
p := NewPlugins()
|
||||
assert.NoError(t, p.load(u.path))
|
||||
assert.NoError(t, p.loadDir(u.dir))
|
||||
assert.Equal(t, u.ee, p)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
shortCut: shift-s
|
||||
confirm: true
|
||||
description: blee
|
||||
scopes:
|
||||
- po
|
||||
- dp
|
||||
command: duh
|
||||
background: false
|
||||
args:
|
||||
- -n
|
||||
- $NAMESPACE
|
||||
- -boolean
|
||||
background: false
|
||||
confirm: true
|
||||
overwriteOutput: true
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
crapola:
|
||||
shortCut: Shift-1
|
||||
description: crapola
|
||||
scopes:
|
||||
- pods
|
||||
command: crapola
|
||||
background: false
|
||||
|
||||
bozo:
|
||||
shortCut: Shift-2
|
||||
description: bozo
|
||||
scopes:
|
||||
- pods
|
||||
- svc
|
||||
command: bozo
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
plugins:
|
||||
blah:
|
||||
shortCut: shift-s
|
||||
confirm: true
|
||||
description: blee
|
||||
scoped:
|
||||
- po
|
||||
- dp
|
||||
command: duh
|
||||
background: false
|
||||
args:
|
||||
- -n
|
||||
- $NAMESPACE
|
||||
- -boolean
|
||||
|
|
@ -52,8 +52,8 @@ const (
|
|||
// GOR tracks a gor logger key.
|
||||
GOR = "gor"
|
||||
|
||||
// Action tracks an action logger key.
|
||||
Action = "action"
|
||||
// Shortcut tracks a shortcut logger key.
|
||||
Shortcut = "shortcut"
|
||||
|
||||
// Page tracks a page logger key.
|
||||
Page = "page"
|
||||
|
|
@ -117,6 +117,9 @@ const (
|
|||
// Key tracks a key logger key.
|
||||
Key = "key"
|
||||
|
||||
// Plugin tracks a plugin logger key.
|
||||
Plugin = "plugin"
|
||||
|
||||
// Component tracks a component logger key.
|
||||
Component = "component"
|
||||
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
unlockedIC = "🔓"
|
||||
unlockedIC = "✍️ "
|
||||
lockedIC = "🔒"
|
||||
)
|
||||
|
||||
// Namespaceable represents a namespaceable model.
|
||||
// Namespaceable tracks namespaces.
|
||||
type Namespaceable interface {
|
||||
// ClusterWide returns true if the model represents resource in all namespaces.
|
||||
ClusterWide() bool
|
||||
|
|
@ -35,7 +35,7 @@ type Namespaceable interface {
|
|||
InNamespace(string) bool
|
||||
}
|
||||
|
||||
// Lister represents a viewable resource.
|
||||
// Lister tracks resource getter.
|
||||
type Lister interface {
|
||||
// Get returns a resource instance.
|
||||
Get(ctx context.Context, path string) (runtime.Object, error)
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ func hotKeyActions(r Runner, aa *ui.KeyActions) error {
|
|||
errs = errors.Join(errs, fmt.Errorf("duplicate hotkey found for %q in %q", hk.ShortCut, k))
|
||||
continue
|
||||
}
|
||||
slog.Debug("Action has been overridden by hotkey",
|
||||
slogs.Action, hk.ShortCut,
|
||||
slog.Debug("HotKey overrode action shortcut",
|
||||
slogs.Shortcut, hk.ShortCut,
|
||||
slogs.Key, k,
|
||||
)
|
||||
}
|
||||
|
|
@ -125,7 +125,7 @@ func pluginActions(r Runner, aa *ui.KeyActions) error {
|
|||
return err
|
||||
}
|
||||
pp := config.NewPlugins()
|
||||
if err := pp.Load(path); err != nil {
|
||||
if err := pp.Load(path, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -135,9 +135,10 @@ func pluginActions(r Runner, aa *ui.KeyActions) error {
|
|||
ro = r.App().Config.IsReadOnly()
|
||||
)
|
||||
for k, plugin := range pp.Plugins {
|
||||
if !inScope(plugin.Scopes, aliases) {
|
||||
if !inScope(plugin.Scopes, aliases) || (ro && plugin.Dangerous) {
|
||||
continue
|
||||
}
|
||||
|
||||
key, err := asKey(plugin.ShortCut)
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
|
|
@ -148,15 +149,12 @@ func pluginActions(r Runner, aa *ui.KeyActions) error {
|
|||
errs = errors.Join(errs, fmt.Errorf("duplicate plugin key found for %q in %q", plugin.ShortCut, k))
|
||||
continue
|
||||
}
|
||||
slog.Debug("Action has been overridden by plugin action",
|
||||
slogs.Action, plugin.ShortCut,
|
||||
slogs.Key, k,
|
||||
slog.Debug("Plugin overrode action shortcut",
|
||||
slogs.Plugin, k,
|
||||
slogs.Key, plugin.ShortCut,
|
||||
)
|
||||
}
|
||||
|
||||
if plugin.Dangerous && ro {
|
||||
continue
|
||||
}
|
||||
aa.Add(key, ui.NewKeyActionWithOpts(
|
||||
plugin.Description,
|
||||
pluginAction(r, plugin),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name: k9s
|
||||
base: core22
|
||||
version: 'v0.40.8'
|
||||
version: 'v0.40.9'
|
||||
summary: K9s is a CLI to view and manage your Kubernetes clusters.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
aliases:
|
||||
dp: deployments
|
||||
sec: v1/secrets
|
||||
jo: jobs
|
||||
cr: clusterroles
|
||||
crb: clusterrolebindings
|
||||
ro: roles
|
||||
rb: rolebindings
|
||||
np: networkpolicies
|
||||
Loading…
Reference in New Issue