Rel v0.50.0 (#3254)

* update deps + add stale issues

* springclean client and dao

* gvr clean up
* perf updates

* springclean render

* update gvr
* perf
* add jq like cust views support

* springclean config and models

* gvr update
* perf

* springclean perf

* springclean ui bits

* update ro icon
* updage gvr
* perf

* springclean watch

* update linter to v2

* update gha workflows

* small clean up

* spring clean
* move pool to internal

* rel notes
mine
Fernand Galiana 2025-04-08 23:40:34 -06:00 committed by GitHub
parent 694c015dc3
commit e55083ba27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
411 changed files with 4980 additions and 4433 deletions

View File

@ -17,9 +17,4 @@ jobs:
stale-issue-label: "stale" stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: 30
days-before-pr-close: 14
stale-pr-label: "stale"
stale-pr-message: "This PR is stale because it has been open for 30 days with no activity."
close-pr-message: "This PR was closed because it has been inactive for 14 days since being marked as stale."
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}

20
.github/workflows/stales-prs.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: Closeout Stale PRs
on:
schedule:
- cron: "0 2 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
days-before-pr-stale: 30
days-before-pr-close: 14
stale-pr-label: "stale"
stale-pr-message: "This PR is stale because it has been open for 30 days with no activity."
close-pr-message: "This PR was closed because it has been inactive for 14 days since being marked as stale."
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,7 +1,7 @@
version: "2" version: "2"
run: run:
concurrency: 8 allow-parallel-runners: true
# timeout for analysis, e.g. 30s, 5m, default is 1m # timeout for analysis, e.g. 30s, 5m, default is 1m
timeout: 5m timeout: 5m
@ -10,137 +10,305 @@ run:
issues-exit-code: 1 issues-exit-code: 1
tests: true tests: true
linters:
enable:
- sloglint
- bodyclose
- copyloopvar
- depguard
- errcheck
- errorlint
- gocheckcompilerdirectives
- gocritic
- godox
- goprintffuncname
- gosec
- govet
- intrange
- ineffassign
- misspell
- noctx
- nolintlint
- revive
- staticcheck
- testifylint
- unconvert
- unparam
- unused
- whitespace
- gocyclo
- funlen
- goconst
- dogsled
- lll
# - dupl
# - gochecknoinits
# - mnd
settings:
dogsled:
max-blank-identifiers: 3
gosec:
excludes:
- G109
- G115
- G204
- G303
sloglint:
no-mixed-args: true
kv-only: true
attr-only: false
no-global: ""
context: ""
static-msg: false
no-raw-keys: true
key-naming-case: camel
forbidden-keys:
- time
- level
- msg
- source
args-on-sep-lines: false
depguard:
rules:
logger:
deny:
# logging is allowed only by logutils.Log,
- pkg: "github.com/sirupsen/logrus"
desc: logging is allowed only by logutils.Log.
- pkg: "github.com/pkg/errors"
desc: Should be replaced by standard lib errors package.
- pkg: "github.com/instana/testify"
desc: It's a fork of github.com/stretchr/testify.
files:
- "!**/pkg/logutils/**.go"
dupl:
threshold: 100
funlen:
lines: -1
statements: 60
goconst:
min-len: 2
min-occurrences: 3
ignore-strings: 'blee|duh|cl-1|ct-1-1'
# gocritic:
# enabled-tags:
# - diagnostic
# - experimental
# - opinionated
# - performance
# - style
# disabled-checks:
# - dupImport # https://github.com/go-critic/go-critic/issues/845
# - ifElseChain
# - octalLiteral
# - whyNoLint
gocyclo:
min-complexity: 35
godox:
keywords:
- FIXME
mnd:
checks:
- argument
- case
- condition
- return
ignored-numbers:
- '0'
- '1'
- '2'
- '3'
ignored-functions:
- strings.SplitN
govet:
settings:
printf:
funcs:
- (github.com/golangci/golangci-lint/v2/pkg/logutils.Log).Infof
- (github.com/golangci/golangci-lint/v2/pkg/logutils.Log).Warnf
- (github.com/golangci/golangci-lint/v2/pkg/logutils.Log).Errorf
- (github.com/golangci/golangci-lint/v2/pkg/logutils.Log).Fatalf
enable:
- nilness
- shadow
errorlint:
asserts: false
lll:
line-length: 170
misspell:
locale: US
ignore-rules:
- "importas"
nolintlint:
allow-unused: false
require-explanation: false
require-specific: true
revive:
rules:
- name: indent-error-flow
- name: unexported-return
disabled: true
- name: unused-parameter
- name: unused-receiver
exclusions:
presets:
- comments
- std-error-handling
- common-false-positives
- legacy
paths:
- test/testdata_etc # test files
- internal/go # extracted from Go code
- internal/x # extracted from x/tools code
- pkg/goformatters/gci/internal # extracted from gci code
- pkg/goanalysis/runner_checker.go # extracted from x/tools code
rules:
- path: (.+)_test\.go
linters:
- dupl
- mnd
- lll
# Based on existing code, the modifications should be limited to make maintenance easier.
- path: pkg/golinters/unused/unused.go
linters: [gocritic]
text: "rangeValCopy: each iteration copies 160 bytes \\(consider pointers or indexing\\)"
# Related to the result of computation but divided multiple times by 1024.
- path: test/bench/bench_test.go
linters: [gosec]
text: "G115: integer overflow conversion uint64 -> int"
# The files created during the tests don't need to be secured.
- path: scripts/website/expand_templates/linters_test.go
linters: [gosec]
text: "G306: Expect WriteFile permissions to be 0600 or less"
# Related to migration command.
- path: pkg/commands/internal/migrate/two/
linters:
- lll
# Related to migration command.
- path: pkg/commands/internal/migrate/
linters:
- gocritic
text: "hugeParam:"
# The codes are close but this is not duplication.
- path: pkg/commands/(formatters|linters).go
linters:
- dupl
formatters: formatters:
enable: enable:
- gci - gci
- gofmt - gofmt
# - gofumpt
- goimports - goimports
# - golines settings:
gofmt:
linters: rewrite-rules:
# disable-all: true - pattern: 'interface{}'
enable: replacement: 'any'
- sloglint goimports:
local-prefixes:
- github.com/golangci/golangci-lint/v2
exclusions: exclusions:
generated: lax
paths: paths:
- third_party$ - test/testdata_etc # test files
- builtin$ - internal/go # extracted from Go code
- examples$ - internal/x # extracted from x/tools code
- \\.(generated\\.deepcopy|pb)\\.go$ - pkg/goformatters/gci/internal # extracted from gci code
- pkg/goanalysis/runner_checker.go # extracted from x/tools code
settings: # linters:
gocyclo: # default: none
min-complexity: 35 # enable:
# - sloglint
govet: # exclusions:
enable: # generated: lax
- nilness # paths:
# - third_party$
# - builtin$
# - examples$
# - \\.(generated\\.deepcopy|pb)\\.go$
goimports: # settings:
local-prefixes: github.com/derailed/k9s # gocyclo:
# min-complexity: 35
unused: # govet:
parameters-are-used: true # enable:
local-variables-are-used: true # - nilness
field-writes-are-uses: true
post-statements-are-reads: true
exported-fields-are-used: true
generated-is-used: true
goheader: # goimports:
values: # local-prefixes: github.com/derailed/k9s
regexp:
PROJECT: 'K9s'
template: |-
SPDX-License-Identifier: Apache-2.0
Copyright Authors of {{ PROJECT }}
gosec: # unused:
includes: # parameters-are-used: true
- G402 # local-variables-are-used: true
# field-writes-are-uses: true
# post-statements-are-reads: true
# exported-fields-are-used: true
# generated-is-used: true
sloglint: # goheader:
# Enforce not mixing key-value pairs and attributes. # values:
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-mixed-arguments # regexp:
# Default: true # PROJECT: 'K9s'
no-mixed-args: true # template: |-
# Enforce using key-value pairs only (overrides no-mixed-args, incompatible with attr-only). # SPDX-License-Identifier: Apache-2.0
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#key-value-pairs-only # Copyright Authors of {{ PROJECT }}
# Default: false
kv-only: true # gosec:
# Enforce using attributes only (overrides no-mixed-args, incompatible with kv-only). # includes:
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#attributes-only # - G402
# Default: false
attr-only: false
# Enforce not using global loggers.
# Values:
# - "": disabled
# - "all": report all global loggers
# - "default": report only the default slog logger
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global
# Default: ""
no-global: ""
# Enforce using methods that accept a context.
# Values:
# - "": disabled
# - "all": report all contextless calls
# - "scope": report only if a context exists in the scope of the outermost function
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only
# Default: ""
context: ""
# Enforce using static values for log messages.
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#static-messages
# Default: false
static-msg: false
# Enforce using constants instead of raw keys.
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-raw-keys
# Default: false
no-raw-keys: true
# Enforce a single key naming convention.
# Values: snake, kebab, camel, pascal
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#key-naming-convention
# Default: ""
key-naming-case: camel
# Enforce not using specific keys.
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#forbidden-keys
# Default: []
forbidden-keys:
- time
- level
- msg
- source
# Enforce putting arguments on separate lines.
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#arguments-on-separate-lines
# Default: false
args-on-sep-lines: false
issues:
# default is true. Enables skipping of directories: # issues:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
# exclude-dirs-use-default: true
# Excluding configuration per-path, per-linter, per-text and per-source # # default is true. Enables skipping of directories:
# exclude-rules: # # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
# - linters: [staticcheck] # # exclude-dirs-use-default: true
# text: "SA1019" # this is rule for deprecated method
# - linters: [staticcheck] # # Excluding configuration per-path, per-linter, per-text and per-source
# text: "SA9003: empty branch" # # exclude-rules:
# # - linters: [staticcheck]
# # text: "SA1019" # this is rule for deprecated method
# - linters: [staticcheck] # # - linters: [staticcheck]
# text: "SA2001: empty critical section" # # text: "SA9003: empty branch"
# - linters: [err113] # # - linters: [staticcheck]
# text: "do not define dynamic errors, use wrapped static errors instead" # This rule to avoid opinionated check fmt.Errorf("text") # # text: "SA2001: empty critical section"
# # Skip goimports check on generated files
# - path: \\.(generated\\.deepcopy|pb)\\.go$ # # - linters: [err113]
# linters: # # text: "do not define dynamic errors, use wrapped static errors instead" # This rule to avoid opinionated check fmt.Errorf("text")
# - goimports # # # Skip goimports check on generated files
# # Skip goheader check on files imported and modified from upstream k8s # # - path: \\.(generated\\.deepcopy|pb)\\.go$
# - path: "pkg/ipam/(cidrset|service)/.+\\.go" # # linters:
# linters: # # - goimports
# - goheader # # # Skip goheader check on files imported and modified from upstream k8s
# # - path: "pkg/ipam/(cidrset|service)/.+\\.go"
# # linters:
# # - goheader

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.40.10 VERSION ?= v0.50.0
IMG_NAME := derailed/k9s IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION} IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -0,0 +1,93 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# Release v0.50
## 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 ♭
* [Afterimage - Justice](https://www.youtube.com/watch?v=9zBJlLbkfzA)
* [This Is The Day - The The](https://www.youtube.com/watch?v=qBF3YqUzYRc)
## 5-O, 5-0... Spring Cleaning In Effect!
☠️ Careful on this upgrade! 🏴‍☠️
We've gone thru lots of code revamp/refactor on this drop, so mileage may vary!!
### K9s Slow?
It looks like K9s performance took a dive in the wrong direction circa v0.40.x releases.
Took a big perf/cleanup pass to improve perf and think this release should help a lot (famous last words...)
> NOTE! As my dear granny use to say: `You can't cook a great meal without trashing the kitchen`,
> So likely I have broken a few things in the process. So thread carefully and report back!
### Now with Super Column Blow!
By general demand, juice up custom views! In a feature we like to refer to as `Super Column Blow...`
As of this drop, you can go full `Chuck Norris` and sprinkle some of your JQ_FU with you custom views.
For example...
```yaml
# views.yaml
views:
v1/pods:
sortColumn: NAME:asc
columns:
- AGE
- NAMESPACE
- NAME
- IMG-VERSION:.spec.containers[0].image|split(":")|.[-1]|R # => Grab the main container image name and pull the image version
# => out into the `IMG-VERSION` right aligned column
```
> NOTE: ☢️ This is very much experimental! Not all JQ queries features are supported!
> (See https://github.com/itchyny/gojq for the details!)
## 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
* [#3226](https://github.com/derailed/k9s/issues/3226) Filter view will show mess when filtering some string
* [#3224](https://github.com/derailed/k9s/issues/3224) Respect kubectl.kubernetes.io/default-container annotation
* [#3222](https://github.com/derailed/k9s/issues/3222) Option to Display Resource Names Without API Version Prefix
* [#3210](https://github.com/derailed/k9s/issues/3210) Description line is buggy
---
## 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!!
* [#3237](https://github.com/derailed/k9s/pull/3237) fix: List CRDs which has k8s.io in their names
* [#3223](https://github.com/derailed/k9s/pull/3223) Fixed skin config ref of in_the_navy to in-the-navy
* [#3110](https://github.com/derailed/k9s/pull/3110) feat: add splashless option to suppress splash screen on start
---
<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)

View File

@ -24,7 +24,7 @@ func infoCmd() *cobra.Command {
} }
} }
func printInfo(cmd *cobra.Command, args []string) error { func printInfo(*cobra.Command, []string) error {
if err := config.InitLocs(); err != nil { if err := config.InitLocs(); err != nil {
return err return err
} }

View File

@ -57,7 +57,7 @@ func init() {
fmt.Printf("Fail to init k9s logs location %s\n", err) fmt.Printf("Fail to init k9s logs location %s\n", err)
} }
rootCmd.SetFlagErrorFunc(func(command *cobra.Command, err error) error { rootCmd.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {
return flagError{err: err} return flagError{err: err}
}) })
@ -75,7 +75,7 @@ func Execute() {
} }
} }
func run(cmd *cobra.Command, args []string) error { func run(*cobra.Command, []string) error {
if err := config.InitLocs(); err != nil { if err := config.InitLocs(); err != nil {
return err return err
} }
@ -378,7 +378,7 @@ func initK8sFlagCompletion() {
return cfg.AuthInfos return cfg.AuthInfos
})) }))
_ = rootCmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, s string) ([]string, cobra.ShellCompDirective) { _ = rootCmd.RegisterFlagCompletionFunc("namespace", func(_ *cobra.Command, _ []string, s string) ([]string, cobra.ShellCompDirective) {
conn := client.NewConfig(k8sFlags) conn := client.NewConfig(k8sFlags)
if c, err := client.InitConnection(conn, slog.Default()); err == nil { if c, err := client.InitConnection(conn, slog.Default()); err == nil {
if nss, err := c.ValidNamespaceNames(); err == nil { if nss, err := c.ValidNamespaceNames(); err == nil {
@ -391,7 +391,7 @@ func initK8sFlagCompletion() {
} }
func k8sFlagCompletion[T any](picker k8sPickerFn[T]) completeFn { func k8sFlagCompletion[T any](picker k8sPickerFn[T]) completeFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
conn := client.NewConfig(k8sFlags) conn := client.NewConfig(k8sFlags)
cfg, err := conn.RawConfig() cfg, err := conn.RawConfig()
if err != nil { if err != nil {

View File

@ -17,7 +17,7 @@ func versionCmd() *cobra.Command {
Use: "version", Use: "version",
Short: "Print version/build info", Short: "Print version/build info",
Long: "Print version/build information", Long: "Print version/build information",
Run: func(cmd *cobra.Command, args []string) { Run: func(*cobra.Command, []string) {
printVersion(short) printVersion(short)
}, },
} }

2
go.mod
View File

@ -15,6 +15,7 @@ require (
github.com/fsnotify/fsnotify v1.8.0 github.com/fsnotify/fsnotify v1.8.0
github.com/fvbommel/sortorder v1.1.0 github.com/fvbommel/sortorder v1.1.0
github.com/go-errors/errors v1.5.1 github.com/go-errors/errors v1.5.1
github.com/itchyny/gojq v0.12.17
github.com/lmittmann/tint v1.0.7 github.com/lmittmann/tint v1.0.7
github.com/mattn/go-colorable v0.1.14 github.com/mattn/go-colorable v0.1.14
github.com/mattn/go-runewidth v0.0.16 github.com/mattn/go-runewidth v0.0.16
@ -192,6 +193,7 @@ require (
github.com/huandu/xstrings v1.5.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/itchyny/timefmt-go v0.1.6 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jinzhu/copier v0.4.0 // indirect github.com/jinzhu/copier v0.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect

4
go.sum
View File

@ -1274,6 +1274,10 @@ github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg=
github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY=
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=

View File

@ -88,12 +88,11 @@ func (a *APIClient) ConnectionOK() bool {
return a.connOK return a.connOK
} }
func makeSAR(ns, gvr, name string) *authorizationv1.SelfSubjectAccessReview { func makeSAR(ns string, gvr *GVR, name string) *authorizationv1.SelfSubjectAccessReview {
if ns == ClusterScope { if ns == ClusterScope {
ns = BlankNamespace ns = BlankNamespace
} }
spec := NewGVR(gvr) res := gvr.GVR()
res := spec.GVR()
return &authorizationv1.SelfSubjectAccessReview{ return &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{ Spec: authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{ ResourceAttributes: &authorizationv1.ResourceAttributes{
@ -101,15 +100,15 @@ func makeSAR(ns, gvr, name string) *authorizationv1.SelfSubjectAccessReview {
Group: res.Group, Group: res.Group,
Version: res.Version, Version: res.Version,
Resource: res.Resource, Resource: res.Resource,
Subresource: spec.SubResource(), Subresource: gvr.SubResource(),
Name: name, Name: name,
}, },
}, },
} }
} }
func makeCacheKey(ns, gvr, n string, vv []string) string { func makeCacheKey(ns string, gvr *GVR, n string, vv []string) string {
return ns + ":" + gvr + ":" + n + "::" + strings.Join(vv, ",") return ns + ":" + gvr.String() + ":" + n + "::" + strings.Join(vv, ",")
} }
// ActiveContext returns the current context name. // ActiveContext returns the current context name.
@ -147,7 +146,7 @@ func (a *APIClient) clearCache() {
} }
// CanI checks if user has access to a certain resource. // CanI checks if user has access to a certain resource.
func (a *APIClient) CanI(ns, gvr, name string, verbs []string) (auth bool, err error) { func (a *APIClient) CanI(ns string, gvr *GVR, name string, verbs []string) (auth bool, err error) {
if !a.getConnOK() { if !a.getConnOK() {
return false, errors.New("ACCESS -- No API server connection") return false, errors.New("ACCESS -- No API server connection")
} }
@ -265,7 +264,7 @@ func (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) {
} }
} }
ok, err := a.CanI(ClusterScope, "v1/namespaces", "", ListAccess) ok, err := a.CanI(ClusterScope, NsGVR, "", ListAccess)
if !ok || err != nil { if !ok || err != nil {
return nil, fmt.Errorf("user not authorized to list all namespaces") return nil, fmt.Errorf("user not authorized to list all namespaces")
} }
@ -281,8 +280,8 @@ func (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) {
return nil, err return nil, err
} }
nns := make(NamespaceNames, len(nn.Items)) nns := make(NamespaceNames, len(nn.Items))
for _, n := range nn.Items { for i := range nn.Items {
nns[n.Name] = struct{}{} nns[nn.Items[i].Name] = struct{}{}
} }
a.cache.Add(cacheNSKey, nns, cacheExpiry) a.cache.Add(cacheNSKey, nns, cacheExpiry)
@ -457,11 +456,11 @@ func (a *APIClient) Dial() (kubernetes.Interface, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if c, err := kubernetes.NewForConfig(cfg); err != nil { c, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, err return nil, err
} else {
a.setClient(c)
} }
a.setClient(c)
return a.getClient(), nil return a.getClient(), nil
} }
@ -586,7 +585,7 @@ func (a *APIClient) reset() {
a.setConnOK(true) a.setConnOK(true)
} }
func (a *APIClient) checkCacheBool(key string) (state bool, ok bool) { func (a *APIClient) checkCacheBool(key string) (state, ok bool) {
v, found := a.cache.Get(key) v, found := a.cache.Get(key)
if !found { if !found {
return return
@ -617,11 +616,11 @@ func (a *APIClient) supportsMetricsResources() error {
if err != nil { if err != nil {
return err return err
} }
for _, grp := range apiGroups.Groups { for i := range apiGroups.Groups {
if grp.Name != metricsapi.GroupName { if apiGroups.Groups[i].Name != metricsapi.GroupName {
continue continue
} }
if checkMetricsVersion(grp) { if checkMetricsVersion(&(apiGroups.Groups[i])) {
supported = true supported = true
return nil return nil
} }
@ -630,7 +629,7 @@ func (a *APIClient) supportsMetricsResources() error {
return metricsUnsupportedErr return metricsUnsupportedErr
} }
func checkMetricsVersion(grp metav1.APIGroup) bool { func checkMetricsVersion(grp *metav1.APIGroup) bool {
for _, v := range grp.Versions { for _, v := range grp.Versions {
for _, supportedVersion := range supportedMetricsAPIVersions { for _, supportedVersion := range supportedMetricsAPIVersions {
if v.Version == supportedVersion { if v.Version == supportedVersion {

View File

@ -14,12 +14,12 @@ import (
func TestMakeSAR(t *testing.T) { func TestMakeSAR(t *testing.T) {
uu := map[string]struct { uu := map[string]struct {
ns string ns string
gvr GVR gvr *GVR
sar *authorizationv1.SelfSubjectAccessReview sar *authorizationv1.SelfSubjectAccessReview
}{ }{
"all-pods": { "all-pods": {
ns: NamespaceAll, ns: NamespaceAll,
gvr: NewGVR("v1/pods"), gvr: PodGVR,
sar: &authorizationv1.SelfSubjectAccessReview{ sar: &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{ Spec: authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{ ResourceAttributes: &authorizationv1.ResourceAttributes{
@ -30,9 +30,10 @@ func TestMakeSAR(t *testing.T) {
}, },
}, },
}, },
"ns-pods": { "ns-pods": {
ns: "fred", ns: "fred",
gvr: NewGVR("v1/pods"), gvr: PodGVR,
sar: &authorizationv1.SelfSubjectAccessReview{ sar: &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{ Spec: authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{ ResourceAttributes: &authorizationv1.ResourceAttributes{
@ -43,9 +44,10 @@ func TestMakeSAR(t *testing.T) {
}, },
}, },
}, },
"clusterscope-ns": { "clusterscope-ns": {
ns: ClusterScope, ns: ClusterScope,
gvr: NewGVR("v1/namespaces"), gvr: NsGVR,
sar: &authorizationv1.SelfSubjectAccessReview{ sar: &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{ Spec: authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{ ResourceAttributes: &authorizationv1.ResourceAttributes{
@ -55,6 +57,7 @@ func TestMakeSAR(t *testing.T) {
}, },
}, },
}, },
"subres-pods": { "subres-pods": {
ns: "fred", ns: "fred",
gvr: NewGVR("v1/pods:logs"), gvr: NewGVR("v1/pods:logs"),
@ -74,7 +77,7 @@ func TestMakeSAR(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) {
assert.Equal(t, u.sar, makeSAR(u.ns, u.gvr.String(), "")) assert.Equal(t, u.sar, makeSAR(u.ns, u.gvr, ""))
}) })
} }
} }
@ -153,7 +156,7 @@ func TestCheckCacheBool(t *testing.T) {
const key = "fred" const key = "fred"
uu := map[string]struct { uu := map[string]struct {
key string key string
val interface{} val any
found, actual, sleep bool found, actual, sleep bool
}{ }{
"setTrue": { "setTrue": {

View File

@ -77,7 +77,7 @@ func (c *Config) clientConfig() clientcmd.ClientConfig {
return c.flags.ToRawKubeConfigLoader() return c.flags.ToRawKubeConfigLoader()
} }
func (c *Config) reset() {} func (*Config) reset() {}
// SwitchContext changes the kubeconfig context to a new cluster. // SwitchContext changes the kubeconfig context to a new cluster.
func (c *Config) SwitchContext(name string) error { func (c *Config) SwitchContext(name string) error {
@ -221,17 +221,17 @@ func (c *Config) DelContext(n string) error {
} }
// RenameContext renames a context. // RenameContext renames a context.
func (c *Config) RenameContext(old string, new string) error { func (c *Config) RenameContext(oldCtx, newCtx string) error {
cfg, err := c.RawConfig() cfg, err := c.RawConfig()
if err != nil { if err != nil {
return err return err
} }
if _, ok := cfg.Contexts[new]; ok { if _, ok := cfg.Contexts[newCtx]; ok {
return fmt.Errorf("context with name %s already exists", new) return fmt.Errorf("context with name %s already exists", newCtx)
} }
cfg.Contexts[new] = cfg.Contexts[old] cfg.Contexts[newCtx] = cfg.Contexts[oldCtx]
delete(cfg.Contexts, old) delete(cfg.Contexts, oldCtx)
acc, err := c.ConfigAccess() acc, err := c.ConfigAccess()
if err != nil { if err != nil {
return err return err
@ -243,8 +243,8 @@ func (c *Config) RenameContext(old string, new string) error {
if err != nil { if err != nil {
return err return err
} }
if current == old { if current == oldCtx {
return c.SwitchContext(new) return c.SwitchContext(newCtx)
} }
return nil return nil
@ -344,9 +344,9 @@ func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) {
// Helpers... // Helpers...
func isSet(s *string) bool { func isSet(s *string) bool {
return s != nil && len(*s) != 0 return s != nil && *s != ""
} }
func areSet(s *[]string) bool { func areSet(ss *[]string) bool {
return s != nil && len(*s) != 0 return ss != nil && len(*ss) != 0
} }

View File

@ -12,9 +12,12 @@ import (
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
) )
var kubeConfig = "./testdata/config"
func init() { func init() {
slog.SetDefault(slog.New(slog.DiscardHandler)) slog.SetDefault(slog.New(slog.DiscardHandler))
} }
@ -45,8 +48,6 @@ func TestCallTimeout(t *testing.T) {
} }
func TestConfigCurrentContext(t *testing.T) { func TestConfigCurrentContext(t *testing.T) {
kubeConfig := "./testdata/config"
uu := map[string]struct { uu := map[string]struct {
context string context string
e string e string
@ -70,14 +71,14 @@ func TestConfigCurrentContext(t *testing.T) {
} }
cfg := client.NewConfig(flags) cfg := client.NewConfig(flags)
ctx, err := cfg.CurrentContextName() ctx, err := cfg.CurrentContextName()
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, u.e, ctx) assert.Equal(t, u.e, ctx)
}) })
} }
} }
func TestConfigCurrentCluster(t *testing.T) { func TestConfigCurrentCluster(t *testing.T) {
name, kubeConfig := "blee", "./testdata/config" name := "blee"
uu := map[string]struct { uu := map[string]struct {
flags *genericclioptions.ConfigFlags flags *genericclioptions.ConfigFlags
cluster string cluster string
@ -102,14 +103,14 @@ func TestConfigCurrentCluster(t *testing.T) {
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
cfg := client.NewConfig(u.flags) cfg := client.NewConfig(u.flags)
ct, err := cfg.CurrentClusterName() ct, err := cfg.CurrentClusterName()
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, u.cluster, ct) assert.Equal(t, u.cluster, ct)
}) })
} }
} }
func TestConfigCurrentUser(t *testing.T) { func TestConfigCurrentUser(t *testing.T) {
name, kubeConfig := "blee", "./testdata/config" name := "blee"
uu := map[string]struct { uu := map[string]struct {
flags *genericclioptions.ConfigFlags flags *genericclioptions.ConfigFlags
user string user string
@ -129,14 +130,13 @@ func TestConfigCurrentUser(t *testing.T) {
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
cfg := client.NewConfig(u.flags) cfg := client.NewConfig(u.flags)
ctx, err := cfg.CurrentUserName() ctx, err := cfg.CurrentUserName()
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, u.user, ctx) assert.Equal(t, u.user, ctx)
}) })
} }
} }
func TestConfigCurrentNamespace(t *testing.T) { func TestConfigCurrentNamespace(t *testing.T) {
kubeConfig := "./testdata/config"
bleeNS, bleeCTX := "blee", "blee" bleeNS, bleeCTX := "blee", "blee"
uu := map[string]struct { uu := map[string]struct {
flags *genericclioptions.ConfigFlags flags *genericclioptions.ConfigFlags
@ -162,7 +162,7 @@ func TestConfigCurrentNamespace(t *testing.T) {
cfg := client.NewConfig(u.flags) cfg := client.NewConfig(u.flags)
ns, err := cfg.CurrentNamespaceName() ns, err := cfg.CurrentNamespaceName()
if ns != "" { if ns != "" {
assert.Nil(t, err) require.NoError(t, err)
} }
assert.Equal(t, u.namespace, ns) assert.Equal(t, u.namespace, ns)
}) })
@ -170,7 +170,6 @@ func TestConfigCurrentNamespace(t *testing.T) {
} }
func TestConfigGetContext(t *testing.T) { func TestConfigGetContext(t *testing.T) {
kubeConfig := "./testdata/config"
uu := map[string]struct { uu := map[string]struct {
cluster string cluster string
err error err error
@ -201,7 +200,7 @@ func TestConfigGetContext(t *testing.T) {
} }
func TestConfigSwitchContext(t *testing.T) { func TestConfigSwitchContext(t *testing.T) {
cluster, kubeConfig := "duh", "./testdata/config" cluster := "duh"
flags := genericclioptions.ConfigFlags{ flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig, KubeConfig: &kubeConfig,
Context: &cluster, Context: &cluster,
@ -209,14 +208,14 @@ func TestConfigSwitchContext(t *testing.T) {
cfg := client.NewConfig(&flags) cfg := client.NewConfig(&flags)
err := cfg.SwitchContext("blee") err := cfg.SwitchContext("blee")
assert.Nil(t, err) require.NoError(t, err)
ctx, err := cfg.CurrentContextName() ctx, err := cfg.CurrentContextName()
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, "blee", ctx) assert.Equal(t, "blee", ctx)
} }
func TestConfigAccess(t *testing.T) { func TestConfigAccess(t *testing.T) {
context, kubeConfig := "duh", "./testdata/config" context := "duh"
flags := genericclioptions.ConfigFlags{ flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig, KubeConfig: &kubeConfig,
Context: &context, Context: &context,
@ -224,12 +223,12 @@ func TestConfigAccess(t *testing.T) {
cfg := client.NewConfig(&flags) cfg := client.NewConfig(&flags)
acc, err := cfg.ConfigAccess() acc, err := cfg.ConfigAccess()
assert.Nil(t, err) require.NoError(t, err)
assert.True(t, len(acc.GetDefaultFilename()) > 0) assert.NotEmpty(t, acc.GetDefaultFilename())
} }
func TestConfigContextNames(t *testing.T) { func TestConfigContextNames(t *testing.T) {
cluster, kubeConfig := "duh", "./testdata/config" cluster := "duh"
flags := genericclioptions.ConfigFlags{ flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig, KubeConfig: &kubeConfig,
Context: &cluster, Context: &cluster,
@ -237,12 +236,12 @@ func TestConfigContextNames(t *testing.T) {
cfg := client.NewConfig(&flags) cfg := client.NewConfig(&flags)
cc, err := cfg.ContextNames() cc, err := cfg.ContextNames()
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, 3, len(cc)) assert.Len(t, cc, 3)
} }
func TestConfigContexts(t *testing.T) { func TestConfigContexts(t *testing.T) {
context, kubeConfig := "duh", "./testdata/config" context := "duh"
flags := genericclioptions.ConfigFlags{ flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig, KubeConfig: &kubeConfig,
Context: &context, Context: &context,
@ -250,39 +249,38 @@ func TestConfigContexts(t *testing.T) {
cfg := client.NewConfig(&flags) cfg := client.NewConfig(&flags)
cc, err := cfg.Contexts() cc, err := cfg.Contexts()
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, 3, len(cc)) assert.Len(t, cc, 3)
} }
func TestConfigDelContext(t *testing.T) { func TestConfigDelContext(t *testing.T) {
assert.NoError(t, cp("./testdata/config.2", "./testdata/config.1")) require.NoError(t, cp("./testdata/config.2", "./testdata/config.1"))
context, kubeConfig := "duh", "./testdata/config.1" context, kubeCfg := "duh", "./testdata/config.1"
flags := genericclioptions.ConfigFlags{ flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig, KubeConfig: &kubeCfg,
Context: &context, Context: &context,
} }
cfg := client.NewConfig(&flags) cfg := client.NewConfig(&flags)
err := cfg.DelContext("fred") err := cfg.DelContext("fred")
assert.NoError(t, err) require.NoError(t, err)
cc, err := cfg.ContextNames() cc, err := cfg.ContextNames()
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, len(cc)) assert.Len(t, cc, 1)
_, ok := cc["blee"] _, ok := cc["blee"]
assert.True(t, ok) assert.True(t, ok)
} }
func TestConfigRestConfig(t *testing.T) { func TestConfigRestConfig(t *testing.T) {
kubeConfig := "./testdata/config"
flags := genericclioptions.ConfigFlags{ flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig, KubeConfig: &kubeConfig,
} }
cfg := client.NewConfig(&flags) cfg := client.NewConfig(&flags)
rc, err := cfg.RESTConfig() rc, err := cfg.RESTConfig()
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, "https://localhost:3002", rc.Host) assert.Equal(t, "https://localhost:3002", rc.Host)
} }
@ -294,12 +292,12 @@ func TestConfigBadConfig(t *testing.T) {
cfg := client.NewConfig(&flags) cfg := client.NewConfig(&flags)
_, err := cfg.RESTConfig() _, err := cfg.RESTConfig()
assert.NotNil(t, err) assert.Error(t, err)
} }
// Helpers... // Helpers...
func cp(src string, dst string) error { func cp(src, dst string) error {
data, err := os.ReadFile(src) data, err := os.ReadFile(src)
if err != nil { if err != nil {
return err return err

View File

@ -11,11 +11,12 @@ import (
"github.com/derailed/k9s/internal/slogs" "github.com/derailed/k9s/internal/slogs"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
) )
var NoGVR = GVR{} var NoGVR = &GVR{}
// GVR represents a kubernetes resource schema as a string. // GVR represents a kubernetes resource schema as a string.
// Format is group/version/resources:subresource. // Format is group/version/resources:subresource.
@ -23,12 +24,29 @@ type GVR struct {
raw, g, v, r, sr string raw, g, v, r, sr string
} }
// NewGVR builds a new gvr from a group, version, resource. type gvrCache map[string]*GVR
func NewGVR(gvr string) GVR {
var g, v, r, sr string
tokens := strings.Split(gvr, ":") func (c gvrCache) add(gvr *GVR) {
raw := gvr if c.get(gvr.String()) == nil {
c[gvr.String()] = gvr
}
}
func (c gvrCache) get(gvrs string) *GVR {
if gvr, ok := c[gvrs]; ok {
return gvr
}
return nil
}
var gvrsCache = make(gvrCache)
// NewGVR builds a new gvr from a group, version, resource.
func NewGVR(path string) *GVR {
raw := path
tokens := strings.Split(path, ":")
var g, v, r, sr string
if len(tokens) == 2 { if len(tokens) == 2 {
raw, sr = tokens[0], tokens[1] raw, sr = tokens[0], tokens[1]
} }
@ -41,34 +59,62 @@ func NewGVR(gvr string) GVR {
case 1: case 1:
r = tokens[0] r = tokens[0]
default: default:
slog.Error("GVR init failed!", slogs.Error, fmt.Errorf("can't parse GVR %q", gvr)) slog.Error("GVR init failed!", slogs.Error, fmt.Errorf("can't parse GVR %q", path))
} }
return GVR{raw: gvr, g: g, v: v, r: r, sr: sr} gvr := GVR{raw: path, g: g, v: v, r: r, sr: sr}
if cgvr := gvrsCache.get(gvr.String()); cgvr != nil {
return cgvr
}
gvrsCache.add(&gvr)
return &gvr
}
func (g *GVR) IsK8sRes() bool {
return strings.Contains(g.raw, "/")
}
// WithSubResource builds a new gvr with a sub resource.
func (g *GVR) WithSubResource(sub string) *GVR {
return NewGVR(g.String() + ":" + sub)
} }
// NewGVRFromMeta builds a gvr from resource metadata. // NewGVRFromMeta builds a gvr from resource metadata.
func NewGVRFromMeta(a metav1.APIResource) GVR { func NewGVRFromMeta(a *metav1.APIResource) *GVR {
return GVR{ return NewGVR(path.Join(a.Group, a.Version, a.Name))
raw: path.Join(a.Group, a.Version, a.Name), }
g: a.Group,
v: a.Version, // NewGVRFromCRD builds a gvr from a custom resource definition.
r: a.Name, func NewGVRFromCRD(crd *apiext.CustomResourceDefinition) map[*GVR]*apiext.CustomResourceDefinitionVersion {
mm := make(map[*GVR]*apiext.CustomResourceDefinitionVersion, len(crd.Spec.Versions))
for _, v := range crd.Spec.Versions {
if v.Served && !v.Deprecated {
gvr := NewGVRFromMeta(&metav1.APIResource{
Kind: crd.Spec.Names.Kind,
Group: crd.Spec.Group,
Name: crd.Spec.Names.Plural,
Version: v.Name,
})
mm[gvr] = &v
}
} }
return mm
} }
// FromGVAndR builds a gvr from a group/version and resource. // FromGVAndR builds a gvr from a group/version and resource.
func FromGVAndR(gv, r string) GVR { func FromGVAndR(gv, r string) *GVR {
return NewGVR(path.Join(gv, r)) return NewGVR(path.Join(gv, r))
} }
// FQN returns a fully qualified resource name. // FQN returns a fully qualified resource name.
func (g GVR) FQN(n string) string { func (g *GVR) FQN(n string) string {
return path.Join(g.AsResourceName(), n) return path.Join(g.AsResourceName(), n)
} }
// AsResourceName returns a resource . separated descriptor in the shape of kind.version.group. // AsResourceName returns a resource . separated descriptor in the shape of kind.version.group.
func (g GVR) AsResourceName() string { func (g *GVR) AsResourceName() string {
if g.g == "" { if g.g == "" {
return g.r return g.r
} }
@ -77,17 +123,17 @@ func (g GVR) AsResourceName() string {
} }
// SubResource returns a sub resource if available. // SubResource returns a sub resource if available.
func (g GVR) SubResource() string { func (g *GVR) SubResource() string {
return g.sr return g.sr
} }
// String returns gvr as string. // String returns gvr as string.
func (g GVR) String() string { func (g *GVR) String() string {
return g.raw return g.raw
} }
// GV returns the group version scheme representation. // GV returns the group version scheme representation.
func (g GVR) GV() schema.GroupVersion { func (g *GVR) GV() schema.GroupVersion {
return schema.GroupVersion{ return schema.GroupVersion{
Group: g.g, Group: g.g,
Version: g.v, Version: g.v,
@ -95,7 +141,7 @@ func (g GVR) GV() schema.GroupVersion {
} }
// GVK returns a full schema representation. // GVK returns a full schema representation.
func (g GVR) GVK() schema.GroupVersionKind { func (g *GVR) GVK() schema.GroupVersionKind {
return schema.GroupVersionKind{ return schema.GroupVersionKind{
Group: g.G(), Group: g.G(),
Version: g.V(), Version: g.V(),
@ -104,7 +150,7 @@ func (g GVR) GVK() schema.GroupVersionKind {
} }
// GVR returns a full schema representation. // GVR returns a full schema representation.
func (g GVR) GVR() schema.GroupVersionResource { func (g *GVR) GVR() schema.GroupVersionResource {
return schema.GroupVersionResource{ return schema.GroupVersionResource{
Group: g.G(), Group: g.G(),
Version: g.V(), Version: g.V(),
@ -113,7 +159,7 @@ func (g GVR) GVR() schema.GroupVersionResource {
} }
// GVSub returns group vervion sub path. // GVSub returns group vervion sub path.
func (g GVR) GVSub() string { func (g *GVR) GVSub() string {
if g.G() == "" { if g.G() == "" {
return g.V() return g.V()
} }
@ -122,7 +168,7 @@ func (g GVR) GVSub() string {
} }
// GR returns a full schema representation. // GR returns a full schema representation.
func (g GVR) GR() *schema.GroupResource { func (g *GVR) GR() *schema.GroupResource {
return &schema.GroupResource{ return &schema.GroupResource{
Group: g.G(), Group: g.G(),
Resource: g.R(), Resource: g.R(),
@ -130,32 +176,32 @@ func (g GVR) GR() *schema.GroupResource {
} }
// V returns the resource version. // V returns the resource version.
func (g GVR) V() string { func (g *GVR) V() string {
return g.v return g.v
} }
// RG returns the resource and group. // RG returns the resource and group.
func (g GVR) RG() (string, string) { func (g *GVR) RG() (resource, group string) {
return g.r, g.g return g.r, g.g
} }
// R returns the resource name. // R returns the resource name.
func (g GVR) R() string { func (g *GVR) R() string {
return g.r return g.r
} }
// G returns the resource group name. // G returns the resource group name.
func (g GVR) G() string { func (g *GVR) G() string {
return g.g return g.g
} }
// IsDecodable checks if the k8s resource has a decodable view // IsDecodable checks if the k8s resource has a decodable view
func (g GVR) IsDecodable() bool { func (g *GVR) IsDecodable() bool {
return g.GVK().Kind == "secrets" return g.GVK().Kind == "secrets"
} }
// GVRs represents a collection of gvr. // GVRs represents a collection of gvr.
type GVRs []GVR type GVRs []*GVR
// Len returns the list size. // Len returns the list size.
func (g GVRs) Len() int { func (g GVRs) Len() int {

View File

@ -15,15 +15,15 @@ import (
func TestGVRSort(t *testing.T) { func TestGVRSort(t *testing.T) {
gg := client.GVRs{ gg := client.GVRs{
client.NewGVR("v1/pods"), client.PodGVR,
client.NewGVR("v1/services"), client.SvcGVR,
client.NewGVR("apps/v1/deployments"), client.DpGVR,
} }
sort.Sort(gg) sort.Sort(gg)
assert.Equal(t, client.GVRs{ assert.Equal(t, client.GVRs{
client.NewGVR("v1/pods"), client.PodGVR,
client.NewGVR("v1/services"), client.SvcGVR,
client.NewGVR("apps/v1/deployments"), client.DpGVR,
}, gg) }, gg)
} }
@ -54,9 +54,9 @@ func TestGVR(t *testing.T) {
gvr string gvr string
e schema.GroupVersionResource e schema.GroupVersionResource
}{ }{
"full": {"apps/v1/deployments", schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}}, "full": {client.DpGVR.String(), schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}},
"core": {"v1/pods", schema.GroupVersionResource{Version: "v1", Resource: "pods"}}, "core": {client.PodGVR.String(), schema.GroupVersionResource{Version: "v1", Resource: "pods"}},
"bork": {"users", schema.GroupVersionResource{Resource: "users"}}, "bork": {client.UsrGVR.String(), schema.GroupVersionResource{Resource: "users"}},
} }
for k := range uu { for k := range uu {
@ -72,9 +72,9 @@ func TestAsGV(t *testing.T) {
gvr string gvr string
e schema.GroupVersion e schema.GroupVersion
}{ }{
"full": {"apps/v1/deployments", schema.GroupVersion{Group: "apps", Version: "v1"}}, "full": {client.DpGVR.String(), schema.GroupVersion{Group: "apps", Version: "v1"}},
"core": {"v1/pods", schema.GroupVersion{Version: "v1"}}, "core": {client.PodGVR.String(), schema.GroupVersion{Version: "v1"}},
"bork": {"users", schema.GroupVersion{}}, "bork": {client.UsrGVR.String(), schema.GroupVersion{}},
} }
for k := range uu { for k := range uu {
@ -90,8 +90,8 @@ func TestNewGVR(t *testing.T) {
g, v, r string g, v, r string
e string e string
}{ }{
"full": {"apps", "v1", "deployments", "apps/v1/deployments"}, "full": {"apps", "v1", "deployments", client.DpGVR.String()},
"core": {"", "v1", "pods", "v1/pods"}, "core": {"", "v1", "pods", client.PodGVR.String()},
} }
for k := range uu { for k := range uu {
@ -107,9 +107,9 @@ func TestGVRAsResourceName(t *testing.T) {
gvr string gvr string
e string e string
}{ }{
"full": {"apps/v1/deployments", "deployments.v1.apps"}, "full": {client.DpGVR.String(), "deployments.v1.apps"},
"core": {"v1/pods", "pods"}, "core": {client.PodGVR.String(), "pods"},
"k9s": {"users", "users"}, "k9s": {client.UsrGVR.String(), "users"},
"empty": {"", ""}, "empty": {"", ""},
} }
@ -126,9 +126,9 @@ func TestToR(t *testing.T) {
gvr string gvr string
e string e string
}{ }{
"full": {"apps/v1/deployments", "deployments"}, "full": {client.DpGVR.String(), "deployments"},
"core": {"v1/pods", "pods"}, "core": {client.PodGVR.String(), "pods"},
"k9s": {"users", "users"}, "k9s": {client.UsrGVR.String(), "users"},
"empty": {"", ""}, "empty": {"", ""},
} }
@ -145,9 +145,9 @@ func TestToG(t *testing.T) {
gvr string gvr string
e string e string
}{ }{
"full": {"apps/v1/deployments", "apps"}, "full": {client.DpGVR.String(), "apps"},
"core": {"v1/pods", ""}, "core": {client.PodGVR.String(), ""},
"k9s": {"users", ""}, "k9s": {client.UsrGVR.String(), ""},
"empty": {"", ""}, "empty": {"", ""},
} }
@ -164,9 +164,9 @@ func TestToV(t *testing.T) {
gvr string gvr string
e string e string
}{ }{
"full": {"apps/v1/deployments", "v1"}, "full": {client.DpGVR.String(), "v1"},
"core": {"v1beta1/pods", "v1beta1"}, "core": {"v1beta1/pods", "v1beta1"},
"k9s": {"users", ""}, "k9s": {client.UsrGVR.String(), ""},
"empty": {"", ""}, "empty": {"", ""},
} }
@ -182,9 +182,9 @@ func TestToString(t *testing.T) {
uu := map[string]struct { uu := map[string]struct {
gvr string gvr string
}{ }{
"full": {"apps/v1/deployments"}, "full": {client.DpGVR.String()},
"core": {"v1beta1/pods"}, "core": {"v1beta1/pods"},
"k9s": {"users"}, "k9s": {client.UsrGVR.String()},
"empty": {""}, "empty": {""},
} }

75
internal/client/gvrs.go Normal file
View File

@ -0,0 +1,75 @@
package client
var (
// Apps...
DpGVR = NewGVR("apps/v1/deployments")
StsGVR = NewGVR("apps/v1/statefulsets")
DsGVR = NewGVR("apps/v1/daemonsets")
RsGVR = NewGVR("apps/v1/replicasets")
// Core...
SaGVR = NewGVR("v1/serviceaccounts")
PvcGVR = NewGVR("v1/persistentvolumeclaims")
PvGVR = NewGVR("v1/persistentvolumes")
CmGVR = NewGVR("v1/configmaps")
SecGVR = NewGVR("v1/secrets")
EvGVR = NewGVR("events.k8s.io/v1/events")
EpGVR = NewGVR("v1/endpoints")
PodGVR = NewGVR("v1/pods")
NsGVR = NewGVR("v1/namespaces")
NodeGVR = NewGVR("v1/nodes")
SvcGVR = NewGVR("v1/services")
// Autoscaling...
HpaGVR = NewGVR("autoscaling/v1/horizontalpodautoscalers")
// Batch...
CjGVR = NewGVR("batch/v1/cronjobs")
JobGVR = NewGVR("batch/v1/jobs")
// Misc...
CrdGVR = NewGVR("apiextensions.k8s.io/v1/customresourcedefinitions")
PcGVR = NewGVR("scheduling.k8s.io/v1/priorityclasses")
NpGVR = NewGVR("networking.k8s.io/v1/networkpolicies")
ScGVR = NewGVR("storage.k8s.io/v1/storageclasses")
// Policy...
PdbGVR = NewGVR("policy/v1/PodDisruptionBudgets")
PspGVR = NewGVR("policy/v1beta1/podsecuritypolicies")
// Metrics...
NmxGVR = NewGVR("metrics.k8s.io/v1beta1/nodes")
PmxGVR = NewGVR("metrics.k8s.io/v1beta1/pods")
// K9s...
CpuGVR = NewGVR("cpu")
MemGVR = NewGVR("memory")
WkGVR = NewGVR("workloads")
CoGVR = NewGVR("containers")
CtGVR = NewGVR("contexts")
RefGVR = NewGVR("references")
PuGVR = NewGVR("pulses")
ScnGVR = NewGVR("scans")
DirGVR = NewGVR("dirs")
PfGVR = NewGVR("portforwards")
SdGVR = NewGVR("screendumps")
BeGVR = NewGVR("benchmarks")
AliGVR = NewGVR("aliases")
XGVR = NewGVR("xrays")
HlpGVR = NewGVR("help")
QGVR = NewGVR("quit")
// Helm...
HmGVR = NewGVR("helm")
HmhGVR = NewGVR("helm-history")
// RBAC...
RbacGVR = NewGVR("rbac")
PolGVR = NewGVR("policy")
UsrGVR = NewGVR("users")
GrpGVR = NewGVR("groups")
CrGVR = NewGVR("rbac.authorization.k8s.io/v1/clusterroles")
CrbGVR = NewGVR("rbac.authorization.k8s.io/v1/clusterrolebindings")
RoGVR = NewGVR("rbac.authorization.k8s.io/v1/roles")
RobGVR = NewGVR("rbac.authorization.k8s.io/v1/rolebindings")
)

View File

@ -32,7 +32,7 @@ func TestMetaFQN(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) {
assert.Equal(t, u.e, client.MetaFQN(u.meta)) assert.Equal(t, u.e, client.MetaFQN(&u.meta))
}) })
} }
} }
@ -60,7 +60,7 @@ func TestCoFQN(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) {
assert.Equal(t, u.e, client.CoFQN(u.meta, u.co)) assert.Equal(t, u.e, client.CoFQN(&u.meta, u.co))
}) })
} }
} }

View File

@ -52,14 +52,14 @@ func IsClusterScoped(ns string) bool {
} }
// Namespaced converts a resource path to namespace and resource name. // Namespaced converts a resource path to namespace and resource name.
func Namespaced(p string) (string, string) { func Namespaced(p string) (ns, name string) {
ns, n := path.Split(p) ns, name = path.Split(p)
return strings.Trim(ns, "/"), n return strings.Trim(ns, "/"), name
} }
// CoFQN returns a fully qualified container name. // CoFQN returns a fully qualified container name.
func CoFQN(m metav1.ObjectMeta, co string) string { func CoFQN(m *metav1.ObjectMeta, co string) string {
return MetaFQN(m) + ":" + co return MetaFQN(m) + ":" + co
} }
@ -72,7 +72,7 @@ func FQN(ns, n string) string {
} }
// 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 {
if m.Namespace == "" { if m.Namespace == "" {
return FQN(ClusterScope, m.Name) return FQN(ClusterScope, m.Name)
} }
@ -90,6 +90,9 @@ func mustHomeDir() string {
} }
func toHostDir(host string) string { func toHostDir(host string) string {
h := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1) h := strings.Replace(
strings.Replace(host, "https://", "", 1),
"http://", "", 1,
)
return toFileName.ReplaceAllString(h, "_") return toFileName.ReplaceAllString(h, "_")
} }

View File

@ -20,8 +20,6 @@ import (
const ( const (
mxCacheSize = 100 mxCacheSize = 100
mxCacheExpiry = 1 * time.Minute mxCacheExpiry = 1 * time.Minute
podMXGVR = "metrics.k8s.io/v1beta1/pods"
nodeMXGVR = "metrics.k8s.io/v1beta1/nodes"
) )
// MetricsDial tracks global metric server handle. // MetricsDial tracks global metric server handle.
@ -57,22 +55,22 @@ func NewMetricsServer(c Connection) *MetricsServer {
} }
// ClusterLoad retrieves all cluster nodes metrics. // ClusterLoad retrieves all cluster nodes metrics.
func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsList, mx *ClusterMetrics) error { func (*MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsList, mx *ClusterMetrics) error {
if nos == nil || nmx == nil { if nos == nil || nmx == nil {
return fmt.Errorf("invalid node or node metrics lists") return fmt.Errorf("invalid node or node metrics lists")
} }
nodeMetrics := make(NodesMetrics, len(nos.Items)) nodeMetrics := make(NodesMetrics, len(nos.Items))
for _, no := range nos.Items { for i := range nos.Items {
nodeMetrics[no.Name] = NodeMetrics{ nodeMetrics[nos.Items[i].Name] = NodeMetrics{
AllocatableCPU: no.Status.Allocatable.Cpu().MilliValue(), AllocatableCPU: nos.Items[i].Status.Allocatable.Cpu().MilliValue(),
AllocatableMEM: no.Status.Allocatable.Memory().Value(), AllocatableMEM: nos.Items[i].Status.Allocatable.Memory().Value(),
} }
} }
for _, mx := range nmx.Items { for i := range nmx.Items {
if node, ok := nodeMetrics[mx.Name]; ok { if node, ok := nodeMetrics[nmx.Items[i].Name]; ok {
node.CurrentCPU = mx.Usage.Cpu().MilliValue() node.CurrentCPU = nmx.Items[i].Usage.Cpu().MilliValue()
node.CurrentMEM = mx.Usage.Memory().Value() node.CurrentMEM = nmx.Items[i].Usage.Memory().Value()
nodeMetrics[mx.Name] = node nodeMetrics[nmx.Items[i].Name] = node
} }
} }
@ -88,7 +86,7 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
return nil return nil
} }
func (m *MetricsServer) checkAccess(ns, gvr, msg string) error { func (m *MetricsServer) checkAccess(ns string, gvr *GVR, msg string) error {
if !m.HasMetrics() { if !m.HasMetrics() {
return errors.New("no metrics-server detected on cluster") return errors.New("no metrics-server detected on cluster")
} }
@ -104,29 +102,31 @@ func (m *MetricsServer) checkAccess(ns, gvr, msg string) error {
} }
// NodesMetrics retrieves metrics for a given set of nodes. // NodesMetrics retrieves metrics for a given set of nodes.
func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeMetricsList, mmx NodesMetrics) { func (*MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeMetricsList, mmx NodesMetrics) {
if nodes == nil || metrics == nil { if nodes == nil || metrics == nil {
return return
} }
for _, no := range nodes.Items { for i := range nodes.Items {
mmx[no.Name] = NodeMetrics{ mmx[nodes.Items[i].Name] = NodeMetrics{
AllocatableCPU: no.Status.Allocatable.Cpu().MilliValue(), AllocatableCPU: nodes.Items[i].Status.Allocatable.Cpu().MilliValue(),
AllocatableMEM: ToMB(no.Status.Allocatable.Memory().Value()), AllocatableMEM: ToMB(nodes.Items[i].Status.Allocatable.Memory().Value()),
AllocatableEphemeral: ToMB(no.Status.Allocatable.StorageEphemeral().Value()), AllocatableEphemeral: ToMB(nodes.Items[i].Status.Allocatable.StorageEphemeral().Value()),
TotalCPU: no.Status.Capacity.Cpu().MilliValue(), TotalCPU: nodes.Items[i].Status.Capacity.Cpu().MilliValue(),
TotalMEM: ToMB(no.Status.Capacity.Memory().Value()), TotalMEM: ToMB(nodes.Items[i].Status.Capacity.Memory().Value()),
TotalEphemeral: ToMB(no.Status.Capacity.StorageEphemeral().Value()), TotalEphemeral: ToMB(nodes.Items[i].Status.Capacity.StorageEphemeral().Value()),
} }
} }
for _, c := range metrics.Items { for i := range metrics.Items {
if mx, ok := mmx[c.Name]; ok { mx, ok := mmx[metrics.Items[i].Name]
mx.CurrentCPU = c.Usage.Cpu().MilliValue() if !ok {
mx.CurrentMEM = ToMB(c.Usage.Memory().Value()) continue
mx.AvailableCPU = mx.AllocatableCPU - mx.CurrentCPU
mx.AvailableMEM = mx.AllocatableMEM - mx.CurrentMEM
mmx[c.Name] = mx
} }
mx.CurrentCPU = metrics.Items[i].Usage.Cpu().MilliValue()
mx.CurrentMEM = ToMB(metrics.Items[i].Usage.Memory().Value())
mx.AvailableCPU = mx.AllocatableCPU - mx.CurrentCPU
mx.AvailableMEM = mx.AllocatableMEM - mx.CurrentMEM
mmx[metrics.Items[i].Name] = mx
} }
} }
@ -151,7 +151,7 @@ func (m *MetricsServer) FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMe
const msg = "user is not authorized to list node metrics" const msg = "user is not authorized to list node metrics"
mx := new(mv1beta1.NodeMetricsList) mx := new(mv1beta1.NodeMetricsList)
if err := m.checkAccess(ClusterScope, nodeMXGVR, msg); err != nil { if err := m.checkAccess(ClusterScope, NmxGVR, msg); err != nil {
return mx, err return mx, err
} }
@ -182,7 +182,7 @@ func (m *MetricsServer) FetchNodeMetrics(ctx context.Context, n string) (*mv1bet
const msg = "user is not authorized to list node metrics" const msg = "user is not authorized to list node metrics"
mx := new(mv1beta1.NodeMetrics) mx := new(mv1beta1.NodeMetrics)
if err := m.checkAccess(ClusterScope, nodeMXGVR, msg); err != nil { if err := m.checkAccess(ClusterScope, NmxGVR, msg); err != nil {
return mx, err return mx, err
} }
@ -222,7 +222,7 @@ func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1be
if ns == NamespaceAll { if ns == NamespaceAll {
ns = BlankNamespace ns = BlankNamespace
} }
if err := m.checkAccess(ns, podMXGVR, msg); err != nil { if err := m.checkAccess(ns, PmxGVR, msg); err != nil {
return mx, err return mx, err
} }
@ -273,7 +273,7 @@ func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1be
if ns == NamespaceAll { if ns == NamespaceAll {
ns = BlankNamespace ns = BlankNamespace
} }
if err := m.checkAccess(ns, podMXGVR, msg); err != nil { if err := m.checkAccess(ns, PmxGVR, msg); err != nil {
return mx, err return mx, err
} }
@ -290,19 +290,19 @@ func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1be
} }
// PodsMetrics retrieves metrics for all pods in a given namespace. // PodsMetrics retrieves metrics for all pods in a given namespace.
func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetrics) { func (*MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetrics) {
if pods == nil { if pods == nil {
return return
} }
// Compute all pod's containers metrics. // Compute all pod's containers metrics.
for _, p := range pods.Items { for i := range pods.Items {
var mx PodMetrics var mx PodMetrics
for _, c := range p.Containers { for _, c := range pods.Items[i].Containers {
mx.CurrentCPU += c.Usage.Cpu().MilliValue() mx.CurrentCPU += c.Usage.Cpu().MilliValue()
mx.CurrentMEM += ToMB(c.Usage.Memory().Value()) mx.CurrentMEM += ToMB(c.Usage.Memory().Value())
} }
mmx[p.Namespace+"/"+p.Name] = mx mmx[pods.Items[i].Namespace+"/"+pods.Items[i].Name] = mx
} }
} }

View File

@ -79,7 +79,7 @@ func TestPodsMetrics(t *testing.T) {
mmx := make(client.PodsMetrics) mmx := make(client.PodsMetrics)
m.PodsMetrics(u.metrics, mmx) m.PodsMetrics(u.metrics, mmx)
assert.Equal(t, u.eSize, len(mmx)) assert.Len(t, mmx, u.eSize)
if u.eSize == 0 { if u.eSize == 0 {
return return
} }
@ -104,7 +104,7 @@ func BenchmarkPodsMetrics(b *testing.B) {
b.ResetTimer() b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()
for n := 0; n < b.N; n++ { for range b.N {
m.PodsMetrics(&metrics, mmx) m.PodsMetrics(&metrics, mmx)
} }
} }
@ -175,7 +175,7 @@ func TestNodesMetrics(t *testing.T) {
mmx := make(client.NodesMetrics) mmx := make(client.NodesMetrics)
m.NodesMetrics(u.nodes, u.metrics, mmx) m.NodesMetrics(u.nodes, u.metrics, mmx)
assert.Equal(t, u.eSize, len(mmx)) assert.Len(t, mmx, u.eSize)
if u.eSize == 0 { if u.eSize == 0 {
return return
} }
@ -206,7 +206,7 @@ func BenchmarkNodesMetrics(b *testing.B) {
b.ResetTimer() b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()
for n := 0; n < b.N; n++ { for range b.N {
m.NodesMetrics(&nodes, &metrics, mmx) m.NodesMetrics(&nodes, &metrics, mmx)
} }
} }
@ -290,7 +290,7 @@ func BenchmarkClusterLoad(b *testing.B) {
var mx client.ClusterMetrics var mx client.ClusterMetrics
b.ResetTimer() b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()
for n := 0; n < b.N; n++ { for range b.N {
_ = m.ClusterLoad(&nodes, &metrics, &mx) _ = m.ClusterLoad(&nodes, &metrics, &mx)
} }
} }

View File

@ -83,7 +83,7 @@ type PodsMetricsMap map[string]*mv1beta1.PodMetrics
// Authorizer checks what a user can or cannot do to a resource. // Authorizer checks what a user can or cannot do to a resource.
type Authorizer interface { type Authorizer interface {
// CanI returns true if the user can use these actions for a given resource. // CanI returns true if the user can use these actions for a given resource.
CanI(ns, gvr, n string, verbs []string) (bool, error) CanI(ns string, gvr *GVR, n string, verbs []string) (bool, error)
} }
// Connection represents a Kubernetes apiserver connection. // Connection represents a Kubernetes apiserver connection.

View File

@ -47,7 +47,7 @@ func TestHighlight(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) {
assert.Equal(t, u.e, string(color.Highlight([]byte(u.text), u.indices, u.color))) assert.Equal(t, u.e, string(color.Highlight(u.text, u.indices, u.color)))
}) })
} }
} }

View File

@ -5,22 +5,25 @@ package config
import ( import (
"errors" "errors"
"fmt"
"io/fs" "io/fs"
"log/slog" "log/slog"
"os" "os"
"sync" "sync"
"github.com/derailed/k9s/internal/client"
"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/derailed/k9s/internal/slogs" "github.com/derailed/k9s/internal/slogs"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/util/sets"
) )
// Alias tracks shortname to GVR mappings. // Alias tracks shortname to GVR mappings.
type Alias map[string]string type Alias map[string]*client.GVR
// ShortNames represents a collection of shortnames for aliases. // ShortNames represents a collection of shortnames for aliases.
type ShortNames map[string][]string type ShortNames map[*client.GVR][]string
// Aliases represents a collection of aliases. // Aliases represents a collection of aliases.
type Aliases struct { type Aliases struct {
@ -35,29 +38,17 @@ func NewAliases() *Aliases {
} }
} }
func (a *Aliases) AliasesFor(s string) []string { func (a *Aliases) AliasesFor(gvr *client.GVR) sets.Set[string] {
aa := make([]string, 0, 10)
a.mx.RLock() a.mx.RLock()
defer a.mx.RUnlock() defer a.mx.RUnlock()
for k, v := range a.Alias {
if v == s { ss := sets.New[string]()
aa = append(aa, k) for alias, aliasGVR := range a.Alias {
if aliasGVR == gvr {
ss.Insert(alias)
} }
} }
return aa
}
// Keys returns all aliases keys.
func (a *Aliases) Keys() []string {
a.mx.RLock()
defer a.mx.RUnlock()
ss := make([]string, 0, len(a.Alias))
for k := range a.Alias {
ss = append(ss, k)
}
return ss return ss
} }
@ -89,24 +80,27 @@ func (a *Aliases) Clear() {
} }
// Get retrieves an alias. // Get retrieves an alias.
func (a *Aliases) Get(k string) (string, bool) { func (a *Aliases) Get(alias string) (*client.GVR, bool) {
a.mx.RLock() a.mx.RLock()
defer a.mx.RUnlock() defer a.mx.RUnlock()
v, ok := a.Alias[k] gvr, ok := a.Alias[alias]
return v, ok if ok && !gvr.IsK8sRes() {
if rgvr, found := a.Alias[gvr.String()]; found {
return rgvr, found
}
}
return gvr, ok
} }
// Define declares a new alias. // Define declares a new alias.
func (a *Aliases) Define(gvr string, aliases ...string) { func (a *Aliases) Define(gvr *client.GVR, aliases ...string) {
if gvr.String() == "deployment" {
fmt.Println("!!YO!!")
}
a.mx.Lock() a.mx.Lock()
defer a.mx.Unlock() defer a.mx.Unlock()
// BOZO!! Could not get full events struct using this api group??
if gvr == "events.k8s.io/v1/events" || gvr == "extensions/v1beta1" {
return
}
for _, alias := range aliases { for _, alias := range aliases {
if _, ok := a.Alias[alias]; !ok && alias != "" { if _, ok := a.Alias[alias]; !ok && alias != "" {
a.Alias[alias] = gvr a.Alias[alias] = gvr
@ -117,12 +111,10 @@ func (a *Aliases) Define(gvr string, aliases ...string) {
// Load K9s aliases. // Load K9s aliases.
func (a *Aliases) Load(path string) error { func (a *Aliases) Load(path string) error {
a.loadDefaultAliases() a.loadDefaultAliases()
f, err := EnsureAliasesCfgFile() f, err := EnsureAliasesCfgFile()
if err != nil { if err != nil {
slog.Error("Unable to gen config aliases", slogs.Error, err) slog.Error("Unable to gen config aliases", slogs.Error, err)
} }
// load global alias file // load global alias file
if err := a.LoadFile(f); err != nil { if err := a.LoadFile(f); err != nil {
return err return err
@ -132,11 +124,18 @@ func (a *Aliases) Load(path string) error {
return a.LoadFile(path) return a.LoadFile(path)
} }
type aliases struct {
Alias map[string]string `yaml:"aliases"`
}
func newAliases(s int) aliases {
return aliases{
Alias: make(map[string]string, s),
}
}
// LoadFile loads alias from a given file. // LoadFile loads alias from a given file.
func (a *Aliases) LoadFile(path string) error { func (a *Aliases) LoadFile(path string) error {
if path == "" {
return nil
}
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return nil return nil
} }
@ -149,23 +148,23 @@ func (a *Aliases) LoadFile(path string) error {
slog.Warn("Aliases validation failed", slogs.Error, err) slog.Warn("Aliases validation failed", slogs.Error, err)
} }
var aa Aliases var aa aliases
if err := yaml.Unmarshal(bb, &aa); err != nil { if err := yaml.Unmarshal(bb, &aa); err != nil {
return err return err
} }
a.mx.Lock() a.mx.Lock()
defer a.mx.Unlock() defer a.mx.Unlock()
for k, v := range aa.Alias { for alias, cmd := range aa.Alias {
a.Alias[k] = v a.Alias[alias] = client.NewGVR(cmd)
} }
return nil return nil
} }
func (a *Aliases) declare(key string, aliases ...string) { func (a *Aliases) declare(gvr *client.GVR, aliases ...string) {
a.Alias[key] = key a.Alias[gvr.String()] = gvr
for _, alias := range aliases { for _, alias := range aliases {
a.Alias[alias] = key a.Alias[alias] = gvr
} }
} }
@ -173,20 +172,20 @@ func (a *Aliases) loadDefaultAliases() {
a.mx.Lock() a.mx.Lock()
defer a.mx.Unlock() defer a.mx.Unlock()
a.declare("help", "h", "?") a.declare(client.HlpGVR, "h", "?")
a.declare("quit", "q", "q!", "qa", "Q") a.declare(client.QGVR, "q", "q!", "qa", "Q")
a.declare("aliases", "alias", "a") a.declare(client.AliGVR, "alias", "a")
a.declare("helm", "charts", "chart", "hm") a.declare(client.HmGVR, "charts", "chart", "hm")
a.declare("dir", "d") a.declare(client.DirGVR, "dir", "d")
a.declare("contexts", "context", "ctx") a.declare(client.CtGVR, "context", "ctx")
a.declare("users", "user", "usr") a.declare(client.UsrGVR, "user", "usr")
a.declare("groups", "group", "grp") a.declare(client.GrpGVR, "group", "grp")
a.declare("portforwards", "portforward", "pf") a.declare(client.PfGVR, "portforward", "pf")
a.declare("benchmarks", "benchmark", "bench") a.declare(client.BeGVR, "benchmark", "bench")
a.declare("screendumps", "screendump", "sd") a.declare(client.SdGVR, "screendump", "sd")
a.declare("pulses", "pulse", "pu", "hz") a.declare(client.PuGVR, "pulse", "pu", "hz")
a.declare("xrays", "xray", "x") a.declare(client.XGVR, "xray", "x")
a.declare("workloads", "workload", "wk") a.declare(client.WkGVR, "workload", "wk")
} }
// Save alias to disk. // Save alias to disk.
@ -200,6 +199,10 @@ func (a *Aliases) SaveAliases(path string) error {
if err := data.EnsureDirPath(path, data.DefaultDirMod); err != nil { if err := data.EnsureDirPath(path, data.DefaultDirMod); err != nil {
return err return err
} }
aa := newAliases(len(a.Alias))
for alias, gvr := range a.Alias {
aa.Alias[alias] = gvr.String()
}
return data.SaveYAML(path, a) return data.SaveYAML(path, aa)
} }

View File

@ -4,44 +4,45 @@
package config_test package config_test
import ( import (
"fmt" "maps"
"os" "os"
"path" "path"
"slices" "slices"
"testing" "testing"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/data"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestAliasClear(t *testing.T) { func TestAliasClear(t *testing.T) {
a := testAliases() a := testAliases()
a.Clear() a.Clear()
assert.Equal(t, 0, len(a.Keys())) assert.Empty(t, slices.Collect(maps.Keys(a.Alias)))
} }
func TestAliasKeys(t *testing.T) { func TestAliasKeys(t *testing.T) {
a := testAliases() a := testAliases()
kk := a.Keys() kk := maps.Keys(a.Alias)
slices.Sort(kk)
assert.Equal(t, []string{"a1", "a11", "a2", "a3"}, kk) assert.Equal(t, []string{"a1", "a11", "a2", "a3"}, slices.Sorted(kk))
} }
func TestAliasShortNames(t *testing.T) { func TestAliasShortNames(t *testing.T) {
a := testAliases() a := testAliases()
ess := config.ShortNames{ ess := config.ShortNames{
"gvr1": []string{"a1", "a11"}, gvr1: []string{"a1", "a11"},
"gvr2": []string{"a2"}, gvr2: []string{"a2"},
"gvr3": []string{"a3"}, gvr3: []string{"a3"},
} }
ss := a.ShortNames() ss := a.ShortNames()
assert.Equal(t, len(ess), len(ss)) assert.Len(t, ss, len(ess))
for k, v := range ss { for k, v := range ss {
v1, ok := ess[k] v1, ok := ess[k]
assert.True(t, ok, fmt.Sprintf("missing: %q", k)) assert.True(t, ok, "missing: %q", k)
slices.Sort(v) slices.Sort(v)
assert.Equal(t, v1, v) assert.Equal(t, v1, v)
} }
@ -49,41 +50,41 @@ func TestAliasShortNames(t *testing.T) {
func TestAliasDefine(t *testing.T) { func TestAliasDefine(t *testing.T) {
type aliasDef struct { type aliasDef struct {
cmd string gvr *client.GVR
aliases []string aliases []string
} }
uu := map[string]struct { uu := map[string]struct {
aliases []aliasDef aliases []aliasDef
registeredCommands map[string]string registeredCommands map[string]*client.GVR
}{ }{
"simple": { "simple": {
aliases: []aliasDef{ aliases: []aliasDef{
{ {
cmd: "one", gvr: client.NewGVR("one"),
aliases: []string{"blee", "duh"}, aliases: []string{"blee", "duh"},
}, },
}, },
registeredCommands: map[string]string{ registeredCommands: map[string]*client.GVR{
"blee": "one", "blee": client.NewGVR("one"),
"duh": "one", "duh": client.NewGVR("one"),
}, },
}, },
"duplicates": { "duplicates": {
aliases: []aliasDef{ aliases: []aliasDef{
{ {
cmd: "one", gvr: client.NewGVR("one"),
aliases: []string{"blee", "duh"}, aliases: []string{"blee", "duh"},
}, { }, {
cmd: "two", gvr: client.NewGVR("two"),
aliases: []string{"blee", "duh", "fred", "zorg"}, aliases: []string{"blee", "duh", "fred", "zorg"},
}, },
}, },
registeredCommands: map[string]string{ registeredCommands: map[string]*client.GVR{
"blee": "one", "blee": client.NewGVR("one"),
"duh": "one", "duh": client.NewGVR("one"),
"fred": "two", "fred": client.NewGVR("two"),
"zorg": "two", "zorg": client.NewGVR("two"),
}, },
}, },
} }
@ -94,7 +95,7 @@ func TestAliasDefine(t *testing.T) {
configAlias := config.NewAliases() configAlias := config.NewAliases()
for _, aliases := range u.aliases { for _, aliases := range u.aliases {
for _, a := range aliases.aliases { for _, a := range aliases.aliases {
configAlias.Define(aliases.cmd, a) configAlias.Define(aliases.gvr, a)
} }
} }
for alias, cmd := range u.registeredCommands { for alias, cmd := range u.registeredCommands {
@ -109,33 +110,39 @@ func TestAliasDefine(t *testing.T) {
func TestAliasesLoad(t *testing.T) { func TestAliasesLoad(t *testing.T) {
config.AppConfigDir = "testdata/aliases" config.AppConfigDir = "testdata/aliases"
a := config.NewAliases() a := config.NewAliases()
require.NoError(t, a.Load(path.Join(config.AppConfigDir, "plain.yaml")))
assert.Nil(t, a.Load(path.Join(config.AppConfigDir, "plain.yaml"))) assert.Len(t, a.Alias, 55)
assert.Equal(t, 54, len(a.Alias))
} }
func TestAliasesSave(t *testing.T) { func TestAliasesSave(t *testing.T) {
assert.NoError(t, data.EnsureFullPath("/tmp/test-aliases", data.DefaultDirMod)) require.NoError(t, data.EnsureFullPath("/tmp/test-aliases", data.DefaultDirMod))
defer assert.NoError(t, os.RemoveAll("/tmp/test-aliases")) defer require.NoError(t, os.RemoveAll("/tmp/test-aliases"))
config.AppAliasesFile = "/tmp/test-aliases/aliases.yaml" config.AppAliasesFile = "/tmp/test-aliases/aliases.yaml"
a := testAliases() a := testAliases()
c := len(a.Alias) c := len(a.Alias)
assert.Equal(t, c, len(a.Alias)) assert.Len(t, a.Alias, c)
assert.Nil(t, a.Save()) require.NoError(t, a.Save())
assert.Nil(t, a.LoadFile(config.AppAliasesFile)) require.NoError(t, a.LoadFile(config.AppAliasesFile))
assert.Equal(t, c, len(a.Alias)) assert.Len(t, a.Alias, c)
} }
// Helpers... // Helpers...
var (
gvr1 = client.NewGVR("gvr1")
gvr2 = client.NewGVR("gvr2")
gvr3 = client.NewGVR("gvr3")
)
func testAliases() *config.Aliases { func testAliases() *config.Aliases {
a := config.NewAliases() a := config.NewAliases()
a.Alias["a1"] = "gvr1" a.Alias["a1"] = gvr1
a.Alias["a11"] = "gvr1" a.Alias["a11"] = gvr1
a.Alias["a2"] = "gvr2" a.Alias["a2"] = gvr2
a.Alias["a3"] = "gvr3" a.Alias["a3"] = gvr3
return a return a
} }

View File

@ -8,6 +8,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestBenchEmpty(t *testing.T) { func TestBenchEmpty(t *testing.T) {
@ -55,11 +56,11 @@ func TestBenchLoad(t *testing.T) {
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
b, err := NewBench(u.file) b, err := NewBench(u.file)
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, u.c, b.Benchmarks.Defaults.C) assert.Equal(t, u.c, b.Benchmarks.Defaults.C)
assert.Equal(t, u.n, b.Benchmarks.Defaults.N) assert.Equal(t, u.n, b.Benchmarks.Defaults.N)
assert.Equal(t, u.svcCount, len(b.Benchmarks.Services)) assert.Len(t, b.Benchmarks.Services, u.svcCount)
assert.Equal(t, u.coCount, len(b.Benchmarks.Containers)) assert.Len(t, b.Benchmarks.Containers, u.coCount)
}) })
} }
} }
@ -105,8 +106,8 @@ func TestBenchServiceLoad(t *testing.T) {
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
b, err := NewBench("testdata/benchmarks/b_good.yaml") b, err := NewBench("testdata/benchmarks/b_good.yaml")
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, 2, len(b.Benchmarks.Services)) assert.Len(t, b.Benchmarks.Services, 2)
svc := b.Benchmarks.Services[u.key] svc := b.Benchmarks.Services[u.key]
assert.Equal(t, u.c, svc.C) assert.Equal(t, u.c, svc.C)
assert.Equal(t, u.n, svc.N) assert.Equal(t, u.n, svc.N)
@ -123,16 +124,16 @@ func TestBenchServiceLoad(t *testing.T) {
func TestBenchReLoad(t *testing.T) { func TestBenchReLoad(t *testing.T) {
b, err := NewBench("testdata/benchmarks/b_containers.yaml") b, err := NewBench("testdata/benchmarks/b_containers.yaml")
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, 2, b.Benchmarks.Defaults.C) assert.Equal(t, 2, b.Benchmarks.Defaults.C)
assert.NoError(t, b.Reload("testdata/benchmarks/b_containers_1.yaml")) require.NoError(t, b.Reload("testdata/benchmarks/b_containers_1.yaml"))
assert.Equal(t, 20, b.Benchmarks.Defaults.C) assert.Equal(t, 20, b.Benchmarks.Defaults.C)
} }
func TestBenchLoadToast(t *testing.T) { func TestBenchLoadToast(t *testing.T) {
_, err := NewBench("testdata/toast.yaml") _, err := NewBench("testdata/toast.yaml")
assert.NotNil(t, err) assert.Error(t, err)
} }
func TestBenchContainerLoad(t *testing.T) { func TestBenchContainerLoad(t *testing.T) {
@ -176,8 +177,8 @@ func TestBenchContainerLoad(t *testing.T) {
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
b, err := NewBench("testdata/benchmarks/b_containers.yaml") b, err := NewBench("testdata/benchmarks/b_containers.yaml")
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, 2, len(b.Benchmarks.Services)) assert.Len(t, b.Benchmarks.Services, 2)
co := b.Benchmarks.Containers[u.key] co := b.Benchmarks.Containers[u.key]
assert.Equal(t, u.c, co.C) assert.Equal(t, u.c, co.C)
assert.Equal(t, u.n, co.N) assert.Equal(t, u.n, co.N)

View File

@ -18,6 +18,7 @@ import (
"github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/config/mock"
m "github.com/petergtz/pegomock" m "github.com/petergtz/pegomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
) )
@ -54,16 +55,16 @@ func TestConfigSave(t *testing.T) {
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
c := mock.NewMockConfig() c := mock.NewMockConfig()
_, err := c.K9s.ActivateContext(u.ct) _, err := c.K9s.ActivateContext(u.ct)
assert.NoError(t, err) require.NoError(t, err)
if u.flags != nil { if u.flags != nil {
c.K9s.Override(u.k9sFlags) c.K9s.Override(u.k9sFlags)
assert.NoError(t, c.Refine(u.flags, u.k9sFlags, client.NewConfig(u.flags))) require.NoError(t, c.Refine(u.flags, u.k9sFlags, client.NewConfig(u.flags)))
} }
assert.NoError(t, c.Save(true)) require.NoError(t, c.Save(true))
bb, err := os.ReadFile(config.AppConfigFile) bb, err := os.ReadFile(config.AppConfigFile)
assert.NoError(t, err) require.NoError(t, err)
ee, err := os.ReadFile("testdata/configs/default.yaml") ee, err := os.ReadFile("testdata/configs/default.yaml")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, string(ee), string(bb)) assert.Equal(t, string(ee), string(bb))
}) })
} }
@ -115,7 +116,7 @@ func TestSetActiveView(t *testing.T) {
c := mock.NewMockConfig() c := mock.NewMockConfig()
_, _ = c.K9s.ActivateContext(u.ct) _, _ = c.K9s.ActivateContext(u.ct)
if u.flags != nil { if u.flags != nil {
assert.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags))) require.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags)))
c.K9s.Override(u.k9sFlags) c.K9s.Override(u.k9sFlags)
} }
c.SetActiveView(u.view) c.SetActiveView(u.view)
@ -158,7 +159,7 @@ func TestActiveContextName(t *testing.T) {
c := mock.NewMockConfig() c := mock.NewMockConfig()
_, _ = c.K9s.ActivateContext(u.ct) _, _ = c.K9s.ActivateContext(u.ct)
if u.flags != nil { if u.flags != nil {
assert.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags))) require.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags)))
c.K9s.Override(u.k9sFlags) c.K9s.Override(u.k9sFlags)
} }
assert.Equal(t, u.e, c.ActiveContextName()) assert.Equal(t, u.e, c.ActiveContextName())
@ -206,7 +207,7 @@ func TestActiveView(t *testing.T) {
c := mock.NewMockConfig() c := mock.NewMockConfig()
_, _ = c.K9s.ActivateContext(u.ct) _, _ = c.K9s.ActivateContext(u.ct)
if u.flags != nil { if u.flags != nil {
assert.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags))) require.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags)))
c.K9s.Override(u.k9sFlags) c.K9s.Override(u.k9sFlags)
} }
assert.Equal(t, u.e, c.ActiveView()) assert.Equal(t, u.e, c.ActiveView())
@ -349,7 +350,7 @@ func TestConfigActivateContext(t *testing.T) {
assert.Equal(t, u.err, err.Error()) assert.Equal(t, u.err, err.Error())
return return
} }
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, u.cl, ct.ClusterName) assert.Equal(t, u.cl, ct.ClusterName)
}) })
} }
@ -393,9 +394,9 @@ func TestConfigCurrentContext(t *testing.T) {
cfg := mock.NewMockConfig() cfg := mock.NewMockConfig()
err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags)) err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags))
assert.NoError(t, err) require.NoError(t, err)
ct, err := cfg.CurrentContext() ct, err := cfg.CurrentContext()
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, u.cluster, ct.ClusterName) assert.Equal(t, u.cluster, ct.ClusterName)
assert.Equal(t, u.namespace, ct.Namespace.Active) assert.Equal(t, u.namespace, ct.Namespace.Active)
}) })
@ -408,7 +409,7 @@ func TestConfigRefine(t *testing.T) {
cl1 = "cl-1" cl1 = "cl-1"
ct2 = "ct-1-2" ct2 = "ct-1-2"
ns1, ns2, nsx = "ns-1", "ns-2", "ns-x" ns1, ns2, nsx = "ns-1", "ns-2", "ns-x"
true = true trueVal = true
) )
uu := map[string]struct { uu := map[string]struct {
@ -465,7 +466,7 @@ func TestConfigRefine(t *testing.T) {
Namespace: &ns2, Namespace: &ns2,
}, },
k9sFlags: &config.Flags{ k9sFlags: &config.Flags{
AllNamespaces: &true, AllNamespaces: &trueVal,
}, },
cluster: "cl-1", cluster: "cl-1",
context: "ct-1-1", context: "ct-1-1",
@ -516,7 +517,7 @@ func TestConfigRefine(t *testing.T) {
if err != nil { if err != nil {
assert.Equal(t, u.err, err.Error()) assert.Equal(t, u.err, err.Error())
} else { } else {
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, u.context, cfg.K9s.ActiveContextName()) assert.Equal(t, u.context, cfg.K9s.ActiveContextName())
assert.Equal(t, u.namespace, cfg.ActiveNamespace()) assert.Equal(t, u.namespace, cfg.ActiveNamespace())
} }
@ -528,14 +529,14 @@ func TestConfigValidate(t *testing.T) {
cfg := mock.NewMockConfig() cfg := mock.NewMockConfig()
cfg.SetConnection(mock.NewMockConnection()) cfg.SetConnection(mock.NewMockConnection())
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true))
cfg.Validate("ct-1-1", "cl-1") cfg.Validate("ct-1-1", "cl-1")
} }
func TestConfigLoad(t *testing.T) { func TestConfigLoad(t *testing.T) {
cfg := mock.NewMockConfig() cfg := mock.NewMockConfig()
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true))
assert.Equal(t, 2, cfg.K9s.RefreshRate) assert.Equal(t, 2, cfg.K9s.RefreshRate)
assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount) assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount)
assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize) assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize)
@ -544,13 +545,13 @@ func TestConfigLoad(t *testing.T) {
func TestConfigLoadCrap(t *testing.T) { func TestConfigLoadCrap(t *testing.T) {
cfg := mock.NewMockConfig() cfg := mock.NewMockConfig()
assert.NotNil(t, cfg.Load("testdata/configs/k9s_not_there.yaml", true)) assert.Error(t, cfg.Load("testdata/configs/k9s_not_there.yaml", true))
} }
func TestConfigSaveFile(t *testing.T) { func TestConfigSaveFile(t *testing.T) {
cfg := mock.NewMockConfig() cfg := mock.NewMockConfig()
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true))
cfg.K9s.RefreshRate = 100 cfg.K9s.RefreshRate = 100
cfg.K9s.ReadOnly = true cfg.K9s.ReadOnly = true
@ -559,28 +560,28 @@ func TestConfigSaveFile(t *testing.T) {
cfg.K9s.UI.UseFullGVRTitle = true cfg.K9s.UI.UseFullGVRTitle = true
cfg.Validate("ct-1-1", "cl-1") cfg.Validate("ct-1-1", "cl-1")
path := filepath.Join("/tmp", "k9s.yaml") path := filepath.Join(os.TempDir(), "k9s.yaml")
assert.NoError(t, cfg.SaveFile(path)) require.NoError(t, cfg.SaveFile(path))
raw, err := os.ReadFile(path) raw, err := os.ReadFile(path)
assert.Nil(t, err) require.NoError(t, err)
ee, err := os.ReadFile("testdata/configs/expected.yaml") ee, err := os.ReadFile("testdata/configs/expected.yaml")
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, string(ee), string(raw)) assert.Equal(t, string(ee), string(raw))
} }
func TestConfigReset(t *testing.T) { func TestConfigReset(t *testing.T) {
cfg := mock.NewMockConfig() cfg := mock.NewMockConfig()
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true))
cfg.Reset() cfg.Reset()
cfg.Validate("ct-1-1", "cl-1") cfg.Validate("ct-1-1", "cl-1")
path := filepath.Join("/tmp", "k9s.yaml") path := filepath.Join(os.TempDir(), "k9s.yaml")
assert.NoError(t, cfg.SaveFile(path)) require.NoError(t, cfg.SaveFile(path))
bb, err := os.ReadFile(path) bb, err := os.ReadFile(path)
assert.Nil(t, err) require.NoError(t, err)
ee, err := os.ReadFile("testdata/configs/k9s.yaml") ee, err := os.ReadFile("testdata/configs/k9s.yaml")
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, string(ee), string(bb)) assert.Equal(t, string(ee), string(bb))
} }

View File

@ -68,7 +68,7 @@ func (c *Context) GetClusterName() string {
} }
// Validate ensures a context config is tip top. // Validate ensures a context config is tip top.
func (c *Context) Validate(conn client.Connection, contextName, clusterName string) { func (c *Context) Validate(conn client.Connection, _, clusterName string) {
c.mx.Lock() c.mx.Lock()
defer c.mx.Unlock() defer c.mx.Unlock()

View File

@ -17,7 +17,7 @@ func TestClusterValidate(t *testing.T) {
assert.Equal(t, "po", c.View.Active) assert.Equal(t, "po", c.View.Active)
assert.Equal(t, "default", c.Namespace.Active) assert.Equal(t, "default", c.Namespace.Active)
assert.Equal(t, 1, len(c.Namespace.Favorites)) assert.Len(t, c.Namespace.Favorites, 1)
assert.Equal(t, []string{"default"}, c.Namespace.Favorites) assert.Equal(t, []string{"default"}, c.Namespace.Favorites)
} }
@ -27,6 +27,6 @@ func TestClusterValidateEmpty(t *testing.T) {
assert.Equal(t, "po", c.View.Active) assert.Equal(t, "po", c.View.Active)
assert.Equal(t, "default", c.Namespace.Active) assert.Equal(t, "default", c.Namespace.Active)
assert.Equal(t, 1, len(c.Namespace.Favorites)) assert.Len(t, c.Namespace.Favorites, 1)
assert.Equal(t, []string{"default"}, c.Namespace.Favorites) assert.Equal(t, []string{"default"}, c.Namespace.Favorites)
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/config/mock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
) )
@ -68,12 +69,12 @@ func TestDirLoad(t *testing.T) {
ks := mock.NewMockKubeSettings(u.flags) ks := mock.NewMockKubeSettings(u.flags)
if strings.Index(u.dir, "/tmp") == 0 { if strings.Index(u.dir, "/tmp") == 0 {
assert.NoError(t, mock.EnsureDir(u.dir)) require.NoError(t, mock.EnsureDir(u.dir))
} }
d := data.NewDir(u.dir) d := data.NewDir(u.dir)
ct, err := ks.CurrentContext() ct, err := ks.CurrentContext()
assert.NoError(t, err) require.NoError(t, err)
if err != nil { if err != nil {
return return
} }

View File

@ -44,8 +44,8 @@ func EnsureDirPath(path string, mod os.FileMode) error {
// EnsureFullPath ensures a directory exist from the given path. // EnsureFullPath ensures a directory exist from the given path.
func EnsureFullPath(path string, mod os.FileMode) error { func EnsureFullPath(path string, mod os.FileMode) error {
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
if err = os.MkdirAll(path, mod); err != nil { if e := os.MkdirAll(path, mod); e != nil {
return err return e
} }
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/data"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestSanitizeFileName(t *testing.T) { func TestSanitizeFileName(t *testing.T) {
@ -65,27 +66,27 @@ func TestHelperInList(t *testing.T) {
func TestEnsureDirPathNone(t *testing.T) { func TestEnsureDirPathNone(t *testing.T) {
const mod = 0744 const mod = 0744
dir := filepath.Join("/tmp", "k9s-test") dir := filepath.Join(os.TempDir(), "k9s-test")
_ = os.Remove(dir) _ = os.Remove(dir)
path := filepath.Join(dir, "duh.yaml") path := filepath.Join(dir, "duh.yaml")
assert.NoError(t, data.EnsureDirPath(path, mod)) require.NoError(t, data.EnsureDirPath(path, mod))
p, err := os.Stat(dir) p, err := os.Stat(dir)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "drwxr--r--", p.Mode().String()) assert.Equal(t, "drwxr--r--", p.Mode().String())
} }
func TestEnsureDirPathNoOpt(t *testing.T) { func TestEnsureDirPathNoOpt(t *testing.T) {
var mod os.FileMode = 0744 var mod os.FileMode = 0744
dir := filepath.Join("/tmp", "k9s-test") dir := filepath.Join(os.TempDir(), "k9s-test")
assert.NoError(t, os.RemoveAll(dir)) require.NoError(t, os.RemoveAll(dir))
assert.NoError(t, os.Mkdir(dir, mod)) require.NoError(t, os.Mkdir(dir, mod))
path := filepath.Join(dir, "duh.yaml") path := filepath.Join(dir, "duh.yaml")
assert.NoError(t, data.EnsureDirPath(path, mod)) require.NoError(t, data.EnsureDirPath(path, mod))
p, err := os.Stat(dir) p, err := os.Stat(dir)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "drwxr--r--", p.Mode().String()) assert.Equal(t, "drwxr--r--", p.Mode().String())
} }

View File

@ -80,7 +80,7 @@ func (n *Namespace) Validate(conn client.Connection) {
} }
// SetActive set the active namespace. // SetActive set the active namespace.
func (n *Namespace) SetActive(ns string, ks KubeSettings) error { func (n *Namespace) SetActive(ns string, _ KubeSettings) error {
if n == nil { if n == nil {
n = NewActiveNamespace(ns) n = NewActiveNamespace(ns)
} }
@ -111,7 +111,7 @@ func (n *Namespace) addFavNS(ns string) {
nfv := make([]string, 0, MaxFavoritesNS) nfv := make([]string, 0, MaxFavoritesNS)
nfv = append(nfv, ns) nfv = append(nfv, ns)
for i := 0; i < len(n.Favorites); i++ { for i := range n.Favorites {
if i+1 < MaxFavoritesNS { if i+1 < MaxFavoritesNS {
nfv = append(nfv, n.Favorites[i]) nfv = append(nfv, n.Favorites[i])
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/config/mock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestNSValidate(t *testing.T) { func TestNSValidate(t *testing.T) {
@ -41,7 +42,7 @@ func TestNsValidateMaxNS(t *testing.T) {
ns.Favorites = allNS ns.Favorites = allNS
ns.Validate(mock.NewMockConnection()) ns.Validate(mock.NewMockConnection())
assert.Equal(t, data.MaxFavoritesNS, len(ns.Favorites)) assert.Len(t, ns.Favorites, data.MaxFavoritesNS)
} }
func TestNSSetActive(t *testing.T) { func TestNSSetActive(t *testing.T) {
@ -61,7 +62,7 @@ func TestNSSetActive(t *testing.T) {
ns := data.NewNamespace() ns := data.NewNamespace()
for _, u := range uu { for _, u := range uu {
err := ns.SetActive(u.ns, mk) err := ns.SetActive(u.ns, mk)
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, u.ns, ns.Active) assert.Equal(t, u.ns, ns.Active)
assert.Equal(t, u.fav, ns.Favorites) assert.Equal(t, u.fav, ns.Favorites)
} }

View File

@ -17,7 +17,7 @@ func NewView() *View {
// Validate a view configuration. // Validate a view configuration.
func (v *View) Validate() { func (v *View) Validate() {
if len(v.Active) == 0 { if v.Active == "" {
v.Active = DefaultView v.Active = DefaultView
} }
} }

View File

@ -178,8 +178,8 @@ func initXDGLocs() error {
AppViewsFile = filepath.Join(AppConfigDir, "views.yaml") AppViewsFile = filepath.Join(AppConfigDir, "views.yaml")
AppSkinsDir = filepath.Join(AppConfigDir, "skins") AppSkinsDir = filepath.Join(AppConfigDir, "skins")
if err := data.EnsureFullPath(AppSkinsDir, data.DefaultDirMod); err != nil { if e := data.EnsureFullPath(AppSkinsDir, data.DefaultDirMod); e != nil {
slog.Warn("No skins dir detected", slogs.Error, err) slog.Warn("No skins dir detected", slogs.Error, e)
} }
AppDumpsDir, err = xdg.StateFile(filepath.Join(AppName, "screen-dumps")) AppDumpsDir, err = xdg.StateFile(filepath.Join(AppName, "screen-dumps"))

View File

@ -11,21 +11,22 @@ import (
"github.com/adrg/xdg" "github.com/adrg/xdg"
"github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/data"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func Test_initXDGLocs(t *testing.T) { func Test_initXDGLocs(t *testing.T) {
tmp, err := UserTmpDir() tmp, err := UserTmpDir()
assert.NoError(t, err) require.NoError(t, err)
assert.NoError(t, os.Unsetenv("XDG_CONFIG_HOME")) require.NoError(t, os.Unsetenv("XDG_CONFIG_HOME"))
assert.NoError(t, os.Unsetenv("XDG_CACHE_HOME")) require.NoError(t, os.Unsetenv("XDG_CACHE_HOME"))
assert.NoError(t, os.Unsetenv("XDG_STATE_HOME")) require.NoError(t, os.Unsetenv("XDG_STATE_HOME"))
assert.NoError(t, os.Unsetenv("XDG_DATA_HOME")) require.NoError(t, os.Unsetenv("XDG_DATA_HOME"))
assert.NoError(t, os.Setenv("XDG_CONFIG_HOME", filepath.Join(tmp, "k9s-xdg", "config"))) require.NoError(t, os.Setenv("XDG_CONFIG_HOME", filepath.Join(tmp, "k9s-xdg", "config")))
assert.NoError(t, os.Setenv("XDG_CACHE_HOME", filepath.Join(tmp, "k9s-xdg", "cache"))) require.NoError(t, os.Setenv("XDG_CACHE_HOME", filepath.Join(tmp, "k9s-xdg", "cache")))
assert.NoError(t, os.Setenv("XDG_STATE_HOME", filepath.Join(tmp, "k9s-xdg", "state"))) require.NoError(t, os.Setenv("XDG_STATE_HOME", filepath.Join(tmp, "k9s-xdg", "state")))
assert.NoError(t, os.Setenv("XDG_DATA_HOME", filepath.Join(tmp, "k9s-xdg", "data"))) require.NoError(t, os.Setenv("XDG_DATA_HOME", filepath.Join(tmp, "k9s-xdg", "data")))
xdg.Reload() xdg.Reload()
uu := map[string]struct { uu := map[string]struct {
@ -55,7 +56,7 @@ func Test_initXDGLocs(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) {
assert.NoError(t, initXDGLocs()) require.NoError(t, initXDGLocs())
assert.Equal(t, u.configDir, AppConfigDir) assert.Equal(t, u.configDir, AppConfigDir)
assert.Equal(t, u.configFile, AppConfigFile) assert.Equal(t, u.configFile, AppConfigFile)
assert.Equal(t, u.benchmarksDir, AppBenchmarksDir) assert.Equal(t, u.benchmarksDir, AppBenchmarksDir)
@ -63,13 +64,13 @@ func Test_initXDGLocs(t *testing.T) {
assert.Equal(t, u.contextHotkeysFile, AppContextHotkeysFile("cl-1", "ct-1-1")) assert.Equal(t, u.contextHotkeysFile, AppContextHotkeysFile("cl-1", "ct-1-1"))
assert.Equal(t, u.contextConfig, AppContextConfig("cl-1", "ct-1-1")) assert.Equal(t, u.contextConfig, AppContextConfig("cl-1", "ct-1-1"))
dir, err := DumpsDir("cl-1", "ct-1-1") dir, err := DumpsDir("cl-1", "ct-1-1")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, u.dumpsDir, dir) assert.Equal(t, u.dumpsDir, dir)
bdir, err := EnsureBenchmarksDir("cl-1", "ct-1-1") bdir, err := EnsureBenchmarksDir("cl-1", "ct-1-1")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, u.benchDir, bdir) assert.Equal(t, u.benchDir, bdir)
hk, err := EnsureHotkeysCfgFile() hk, err := EnsureHotkeysCfgFile()
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, u.hkFile, hk) assert.Equal(t, u.hkFile, hk)
}) })
} }

View File

@ -12,11 +12,12 @@ import (
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/data"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestInitLogLoc(t *testing.T) { func TestInitLogLoc(t *testing.T) {
tmp, err := config.UserTmpDir() tmp, err := config.UserTmpDir()
assert.NoError(t, err) require.NoError(t, err)
uu := map[string]struct { uu := map[string]struct {
dir string dir string
@ -39,33 +40,33 @@ func TestInitLogLoc(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) {
assert.NoError(t, os.Unsetenv(config.K9sEnvLogsDir)) require.NoError(t, os.Unsetenv(config.K9sEnvLogsDir))
assert.NoError(t, os.Unsetenv("XDG_STATE_HOME")) require.NoError(t, os.Unsetenv("XDG_STATE_HOME"))
assert.NoError(t, os.Unsetenv(config.K9sEnvConfigDir)) require.NoError(t, os.Unsetenv(config.K9sEnvConfigDir))
switch k { switch k {
case "log-env": case "log-env":
assert.NoError(t, os.Setenv(config.K9sEnvLogsDir, u.dir)) require.NoError(t, os.Setenv(config.K9sEnvLogsDir, u.dir))
case "xdg-env": case "xdg-env":
assert.NoError(t, os.Setenv("XDG_STATE_HOME", u.dir)) require.NoError(t, os.Setenv("XDG_STATE_HOME", u.dir))
xdg.Reload() xdg.Reload()
case "cfg-env": case "cfg-env":
assert.NoError(t, os.Setenv(config.K9sEnvConfigDir, u.dir)) require.NoError(t, os.Setenv(config.K9sEnvConfigDir, u.dir))
} }
err := config.InitLogLoc() err := config.InitLogLoc()
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, u.e, config.AppLogFile) assert.Equal(t, u.e, config.AppLogFile)
assert.NoError(t, os.RemoveAll(config.AppLogFile)) require.NoError(t, os.RemoveAll(config.AppLogFile))
}) })
} }
} }
func TestEnsureBenchmarkCfg(t *testing.T) { func TestEnsureBenchmarkCfg(t *testing.T) {
assert.NoError(t, os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config")) require.NoError(t, os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config"))
assert.NoError(t, config.InitLocs()) require.NoError(t, config.InitLocs())
defer assert.NoError(t, os.RemoveAll("/tmp/test-config")) defer require.NoError(t, os.RemoveAll("/tmp/test-config"))
assert.NoError(t, data.EnsureFullPath("/tmp/test-config/clusters/cl-1/ct-2", data.DefaultDirMod)) require.NoError(t, data.EnsureFullPath("/tmp/test-config/clusters/cl-1/ct-2", data.DefaultDirMod))
assert.NoError(t, os.WriteFile("/tmp/test-config/clusters/cl-1/ct-2/benchmarks.yaml", []byte{}, data.DefaultFileMod)) require.NoError(t, os.WriteFile("/tmp/test-config/clusters/cl-1/ct-2/benchmarks.yaml", []byte{}, data.DefaultFileMod))
uu := map[string]struct { uu := map[string]struct {
cluster, context string cluster, context string
@ -88,10 +89,10 @@ func TestEnsureBenchmarkCfg(t *testing.T) {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
f, err := config.EnsureBenchmarksCfgFile(u.cluster, u.context) f, err := config.EnsureBenchmarksCfgFile(u.cluster, u.context)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, u.f, f) assert.Equal(t, u.f, f)
bb, err := os.ReadFile(f) bb, err := os.ReadFile(f)
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, u.e, string(bb)) assert.Equal(t, u.e, string(bb))
}) })
} }
@ -99,7 +100,7 @@ func TestEnsureBenchmarkCfg(t *testing.T) {
func TestSkinFileFromName(t *testing.T) { func TestSkinFileFromName(t *testing.T) {
config.AppSkinsDir = "/tmp/k9s-test/skins" config.AppSkinsDir = "/tmp/k9s-test/skins"
defer assert.NoError(t, os.RemoveAll("/tmp/k9s-test/skins")) defer require.NoError(t, os.RemoveAll("/tmp/k9s-test/skins"))
uu := map[string]struct { uu := map[string]struct {
n string n string

View File

@ -23,7 +23,7 @@ func IsBoolSet(b *bool) bool {
} }
func isStringSet(s *string) bool { func isStringSet(s *string) bool {
return s != nil && len(*s) > 0 return s != nil && *s != ""
} }
func isYamlFile(file string) bool { func isYamlFile(file string) bool {

View File

@ -8,18 +8,18 @@ import (
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestHotKeyLoad(t *testing.T) { func TestHotKeyLoad(t *testing.T) {
h := config.NewHotKeys() h := config.NewHotKeys()
assert.NoError(t, h.LoadHotKeys("testdata/hotkeys/hotkeys.yaml")) require.NoError(t, h.LoadHotKeys("testdata/hotkeys/hotkeys.yaml"))
assert.Len(t, h.HotKey, 1)
assert.Equal(t, 1, len(h.HotKey))
k, ok := h.HotKey["pods"] k, ok := h.HotKey["pods"]
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, "shift-0", k.ShortCut) assert.Equal(t, "shift-0", k.ShortCut)
assert.Equal(t, "Launch pod view", k.Description) assert.Equal(t, "Launch pod view", k.Description)
assert.Equal(t, "pods", k.Command) assert.Equal(t, "pods", k.Command)
assert.Equal(t, true, k.KeepHistory) assert.True(t, k.KeepHistory)
} }

View File

@ -4,23 +4,22 @@
package json_test package json_test
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/derailed/k9s/internal/config/json" "github.com/derailed/k9s/internal/config/json"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestValidatePluginSnippet(t *testing.T) { func TestValidatePluginSnippet(t *testing.T) {
plugPath := "testdata/plugins/snippet.yaml" plugPath := "testdata/plugins/snippet.yaml"
bb, err := os.ReadFile(plugPath) bb, err := os.ReadFile(plugPath)
assert.NoError(t, err) require.NoError(t, err)
p := json.NewValidator() p := json.NewValidator()
assert.NoError(t, p.Validate(json.PluginSchema, bb), plugPath) require.NoError(t, p.Validate(json.PluginSchema, bb), plugPath)
} }
func TestValidatePlugins(t *testing.T) { func TestValidatePlugins(t *testing.T) {
@ -51,7 +50,7 @@ func TestValidatePlugins(t *testing.T) {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
bb, err := os.ReadFile(u.path) bb, err := os.ReadFile(u.path)
assert.NoError(t, err) require.NoError(t, err)
v := json.NewValidator() v := json.NewValidator()
if err := v.Validate(u.schema, bb); err != nil { if err := v.Validate(u.schema, bb); err != nil {
assert.Equal(t, u.err, err.Error()) assert.Equal(t, u.err, err.Error())
@ -63,7 +62,7 @@ func TestValidatePlugins(t *testing.T) {
func TestValidatePluginDir(t *testing.T) { func TestValidatePluginDir(t *testing.T) {
plugDir := "../../../plugins" plugDir := "../../../plugins"
ee, err := os.ReadDir(plugDir) ee, err := os.ReadDir(plugDir)
assert.NoError(t, err) require.NoError(t, err)
for _, e := range ee { for _, e := range ee {
if e.IsDir() { if e.IsDir() {
continue continue
@ -72,31 +71,31 @@ func TestValidatePluginDir(t *testing.T) {
if ext == ".md" { if ext == ".md" {
continue continue
} }
assert.True(t, ext == ".yaml", fmt.Sprintf("expected yaml file: %q", e.Name())) assert.Equal(t, ".yaml", ext, "expected yaml file: %q", e.Name())
assert.False(t, strings.Contains(e.Name(), "_"), fmt.Sprintf("underscore in: %q", e.Name())) assert.NotContains(t, "_", e.Name(), "underscore in: %q", e.Name())
bb, err := os.ReadFile(filepath.Join(plugDir, e.Name())) bb, err := os.ReadFile(filepath.Join(plugDir, e.Name()))
assert.NoError(t, err) require.NoError(t, err)
p := json.NewValidator() p := json.NewValidator()
assert.NoError(t, p.Validate(json.PluginsSchema, bb), e.Name()) require.NoError(t, p.Validate(json.PluginsSchema, bb), e.Name())
} }
} }
func TestValidateSkinDir(t *testing.T) { func TestValidateSkinDir(t *testing.T) {
skinDir := "../../../skins" skinDir := "../../../skins"
ee, err := os.ReadDir(skinDir) ee, err := os.ReadDir(skinDir)
assert.NoError(t, err) require.NoError(t, err)
p := json.NewValidator() p := json.NewValidator()
for _, e := range ee { for _, e := range ee {
if e.IsDir() { if e.IsDir() {
continue continue
} }
ext := filepath.Ext(e.Name()) ext := filepath.Ext(e.Name())
assert.True(t, ext == ".yaml", fmt.Sprintf("expected yaml file: %q", e.Name())) assert.Equal(t, ".yaml", ext, "expected yaml file: %q", e.Name())
assert.True(t, !strings.Contains(e.Name(), "_"), fmt.Sprintf("underscore in: %q", e.Name())) assert.NotContains(t, "_", e.Name(), "underscore in: %q", e.Name())
bb, err := os.ReadFile(filepath.Join(skinDir, e.Name())) bb, err := os.ReadFile(filepath.Join(skinDir, e.Name()))
assert.NoError(t, err) require.NoError(t, err)
assert.NoError(t, p.Validate(json.SkinSchema, bb), e.Name()) require.NoError(t, p.Validate(json.SkinSchema, bb), e.Name())
} }
} }
@ -119,7 +118,7 @@ func TestValidateSkin(t *testing.T) {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
bb, err := os.ReadFile(u.f) bb, err := os.ReadFile(u.f)
assert.NoError(t, err) require.NoError(t, err)
if err := v.Validate(json.SkinSchema, bb); err != nil { if err := v.Validate(json.SkinSchema, bb); err != nil {
assert.Equal(t, u.err, err.Error()) assert.Equal(t, u.err, err.Error())
} }
@ -146,7 +145,7 @@ func TestValidateK9s(t *testing.T) {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
bb, err := os.ReadFile(u.f) bb, err := os.ReadFile(u.f)
assert.NoError(t, err) require.NoError(t, err)
if err := v.Validate(json.K9sSchema, bb); err != nil { if err := v.Validate(json.K9sSchema, bb); err != nil {
assert.Equal(t, u.err, err.Error()) assert.Equal(t, u.err, err.Error())
} }
@ -174,7 +173,7 @@ Additional property namespaces is not allowed`,
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
bb, err := os.ReadFile(u.f) bb, err := os.ReadFile(u.f)
assert.NoError(t, err) require.NoError(t, err)
if err := v.Validate(json.ContextSchema, bb); err != nil { if err := v.Validate(json.ContextSchema, bb); err != nil {
assert.Equal(t, u.err, err.Error()) assert.Equal(t, u.err, err.Error())
} }
@ -202,7 +201,7 @@ aliases is required`,
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
bb, err := os.ReadFile(u.f) bb, err := os.ReadFile(u.f)
assert.NoError(t, err) require.NoError(t, err)
if err := v.Validate(json.AliasesSchema, bb); err != nil { if err := v.Validate(json.AliasesSchema, bb); err != nil {
assert.Equal(t, u.err, err.Error()) assert.Equal(t, u.err, err.Error())
} }
@ -232,7 +231,7 @@ columns is required`,
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
bb, err := os.ReadFile(u.f) bb, err := os.ReadFile(u.f)
assert.NoError(t, err) require.NoError(t, err)
if err := v.Validate(json.ViewsSchema, bb); err != nil { if err := v.Validate(json.ViewsSchema, bb); err != nil {
assert.Equal(t, u.err, err.Error()) assert.Equal(t, u.err, err.Error())
} }

View File

@ -24,14 +24,14 @@ type K9s struct {
LiveViewAutoRefresh bool `json:"liveViewAutoRefresh" yaml:"liveViewAutoRefresh"` LiveViewAutoRefresh bool `json:"liveViewAutoRefresh" yaml:"liveViewAutoRefresh"`
ScreenDumpDir string `json:"screenDumpDir" yaml:"screenDumpDir,omitempty"` ScreenDumpDir string `json:"screenDumpDir" yaml:"screenDumpDir,omitempty"`
RefreshRate int `json:"refreshRate" yaml:"refreshRate"` RefreshRate int `json:"refreshRate" yaml:"refreshRate"`
MaxConnRetry int `json:"maxConnRetry" yaml:"maxConnRetry"` MaxConnRetry int32 `json:"maxConnRetry" yaml:"maxConnRetry"`
ReadOnly bool `json:"readOnly" yaml:"readOnly"` ReadOnly bool `json:"readOnly" yaml:"readOnly"`
NoExitOnCtrlC bool `json:"noExitOnCtrlC" yaml:"noExitOnCtrlC"` NoExitOnCtrlC bool `json:"noExitOnCtrlC" yaml:"noExitOnCtrlC"`
PortForwardAddress string `yaml:"portForwardAddress"` PortForwardAddress string `yaml:"portForwardAddress"`
UI UI `json:"ui" yaml:"ui"` UI UI `json:"ui" yaml:"ui"`
SkipLatestRevCheck bool `json:"skipLatestRevCheck" yaml:"skipLatestRevCheck"` SkipLatestRevCheck bool `json:"skipLatestRevCheck" yaml:"skipLatestRevCheck"`
DisablePodCounting bool `json:"disablePodCounting" yaml:"disablePodCounting"` DisablePodCounting bool `json:"disablePodCounting" yaml:"disablePodCounting"`
ShellPod ShellPod `json:"shellPod" yaml:"shellPod"` ShellPod *ShellPod `json:"shellPod" yaml:"shellPod"`
ImageScans ImageScans `json:"imageScans" yaml:"imageScans"` ImageScans ImageScans `json:"imageScans" yaml:"imageScans"`
Logger Logger `json:"logger" yaml:"logger"` Logger Logger `json:"logger" yaml:"logger"`
Thresholds Threshold `json:"thresholds" yaml:"thresholds"` Thresholds Threshold `json:"thresholds" yaml:"thresholds"`
@ -95,7 +95,11 @@ func (k *K9s) Save(contextName, clusterName string, force bool) error {
) )
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) || force { if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) || force {
slog.Debug("[CONFIG] Saving context config to disk", slogs.Path, path, slogs.Cluster, k.getActiveConfig().Context.GetClusterName(), slogs.Context, k.getActiveContextName()) slog.Debug("[CONFIG] Saving context config to disk",
slogs.Path, path,
slogs.Cluster, k.getActiveConfig().Context.GetClusterName(),
slogs.Context, k.getActiveContextName(),
)
return k.dir.Save(path, k.getActiveConfig()) return k.dir.Save(path, k.getActiveConfig())
} }
@ -298,8 +302,8 @@ func (k *K9s) Override(k9sFlags *Flags) {
k.manualReadOnly = k9sFlags.ReadOnly k.manualReadOnly = k9sFlags.ReadOnly
} }
if k9sFlags.Write != nil && *k9sFlags.Write { if k9sFlags.Write != nil && *k9sFlags.Write {
var false bool var falseVal bool
k.manualReadOnly = &false k.manualReadOnly = &falseVal
} }
k.manualCommand = k9sFlags.Command k.manualCommand = k9sFlags.Command
k.manualScreenDumpDir = k9sFlags.ScreenDumpDir k.manualScreenDumpDir = k9sFlags.ScreenDumpDir
@ -382,7 +386,7 @@ func (k *K9s) Validate(c client.Connection, contextName, clusterName string) {
if k.getActiveConfig() == nil { if k.getActiveConfig() == nil {
_, _ = k.ActivateContext(contextName) _, _ = k.ActivateContext(contextName)
} }
k.ShellPod = k.ShellPod.Validate() k.ShellPod.Validate()
k.Logger = k.Logger.Validate() k.Logger = k.Logger.Validate()
k.Thresholds = k.Thresholds.Validate() k.Thresholds = k.Thresholds.Validate()

View File

@ -7,13 +7,14 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func Test_k9sOverrides(t *testing.T) { func Test_k9sOverrides(t *testing.T) {
var ( var (
true = true trueVal = true
cmd = "po" cmd = "po"
dir = "/tmp/blee" dir = "/tmp/blee"
) )
uu := map[string]struct { uu := map[string]struct {
@ -71,15 +72,15 @@ func Test_k9sOverrides(t *testing.T) {
Headless: false, Headless: false,
Logoless: false, Logoless: false,
Crumbsless: false, Crumbsless: false,
manualHeadless: &true, manualHeadless: &trueVal,
manualLogoless: &true, manualLogoless: &trueVal,
manualCrumbsless: &true, manualCrumbsless: &trueVal,
manualSplashless: &true, manualSplashless: &trueVal,
}, },
SkipLatestRevCheck: false, SkipLatestRevCheck: false,
DisablePodCounting: false, DisablePodCounting: false,
manualRefreshRate: 100, manualRefreshRate: 100,
manualReadOnly: &true, manualReadOnly: &trueVal,
manualCommand: &cmd, manualCommand: &cmd,
manualScreenDumpDir: &dir, manualScreenDumpDir: &dir,
}, },
@ -123,7 +124,7 @@ func Test_screenDumpDirOverride(t *testing.T) {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
cfg := NewConfig(nil) cfg := NewConfig(nil)
assert.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true)) require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true))
cfg.K9s.manualScreenDumpDir = &u.dir cfg.K9s.manualScreenDumpDir = &u.dir
assert.Equal(t, u.e, cfg.K9s.AppScreenDumpDir()) assert.Equal(t, u.e, cfg.K9s.AppScreenDumpDir())

View File

@ -10,6 +10,7 @@ import (
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/config/mock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
) )
@ -94,7 +95,7 @@ func TestK9sMerge(t *testing.T) {
UI: config.UI{}, UI: config.UI{},
SkipLatestRevCheck: false, SkipLatestRevCheck: false,
DisablePodCounting: false, DisablePodCounting: false,
ShellPod: config.ShellPod{}, ShellPod: new(config.ShellPod),
ImageScans: config.ImageScans{}, ImageScans: config.ImageScans{},
Logger: config.Logger{}, Logger: config.Logger{},
Thresholds: nil, Thresholds: nil,
@ -135,14 +136,14 @@ func TestContextScreenDumpDir(t *testing.T) {
cfg := mock.NewMockConfig() cfg := mock.NewMockConfig()
_, err := cfg.K9s.ActivateContext("ct-1-1") _, err := cfg.K9s.ActivateContext("ct-1-1")
assert.NoError(t, err) require.NoError(t, err)
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true))
assert.Equal(t, "/tmp/k9s-test/screen-dumps/cl-1/ct-1-1", cfg.K9s.ContextScreenDumpDir()) assert.Equal(t, "/tmp/k9s-test/screen-dumps/cl-1/ct-1-1", cfg.K9s.ContextScreenDumpDir())
} }
func TestAppScreenDumpDir(t *testing.T) { func TestAppScreenDumpDir(t *testing.T) {
cfg := mock.NewMockConfig() cfg := mock.NewMockConfig()
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true))
assert.Equal(t, "/tmp/k9s-test/screen-dumps", cfg.K9s.AppScreenDumpDir()) assert.Equal(t, "/tmp/k9s-test/screen-dumps", cfg.K9s.AppScreenDumpDir())
} }

View File

@ -89,7 +89,7 @@ func (m mockKubeSettings) CurrentContextName() (string, error) {
func (m mockKubeSettings) CurrentClusterName() (string, error) { func (m mockKubeSettings) CurrentClusterName() (string, error) {
return *m.flags.ClusterName, nil return *m.flags.ClusterName, nil
} }
func (m mockKubeSettings) CurrentNamespaceName() (string, error) { func (mockKubeSettings) CurrentNamespaceName() (string, error) {
return "default", nil return "default", nil
} }
func (m mockKubeSettings) GetContext(s string) (*api.Context, error) { func (m mockKubeSettings) GetContext(s string) (*api.Context, error) {
@ -111,7 +111,7 @@ func (m mockKubeSettings) ContextNames() (map[string]struct{}, error) {
return mm, nil return mm, nil
} }
func (m mockKubeSettings) SetProxy(proxy func(*http.Request) (*url.URL, error)) {} func (mockKubeSettings) SetProxy(func(*http.Request) (*url.URL, error)) {}
type mockConnection struct { type mockConnection struct {
ct string ct string
@ -124,57 +124,57 @@ func NewMockConnectionWithContext(ct string) mockConnection {
return mockConnection{ct: ct} return mockConnection{ct: ct}
} }
func (m mockConnection) CanI(ns, gvr, n string, verbs []string) (bool, error) { func (mockConnection) CanI(string, *client.GVR, string, []string) (bool, error) {
return true, nil return true, nil
} }
func (m mockConnection) Config() *client.Config { func (mockConnection) Config() *client.Config {
return nil return nil
} }
func (m mockConnection) ConnectionOK() bool { func (mockConnection) ConnectionOK() bool {
return false return false
} }
func (m mockConnection) Dial() (kubernetes.Interface, error) { func (mockConnection) Dial() (kubernetes.Interface, error) {
return nil, nil return nil, nil
} }
func (m mockConnection) DialLogs() (kubernetes.Interface, error) { func (mockConnection) DialLogs() (kubernetes.Interface, error) {
return nil, nil return nil, nil
} }
func (m mockConnection) SwitchContext(ctx string) error { func (mockConnection) SwitchContext(string) error {
return nil return nil
} }
func (m mockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { func (mockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) {
return nil, nil return nil, nil
} }
func (m mockConnection) RestConfig() (*restclient.Config, error) { func (mockConnection) RestConfig() (*restclient.Config, error) {
return nil, nil return nil, nil
} }
func (m mockConnection) MXDial() (*versioned.Clientset, error) { func (mockConnection) MXDial() (*versioned.Clientset, error) {
return nil, nil return nil, nil
} }
func (m mockConnection) DynDial() (dynamic.Interface, error) { func (mockConnection) DynDial() (dynamic.Interface, error) {
return nil, nil return nil, nil
} }
func (m mockConnection) HasMetrics() bool { func (mockConnection) HasMetrics() bool {
return false return false
} }
func (m mockConnection) ValidNamespaceNames() (client.NamespaceNames, error) { func (mockConnection) ValidNamespaceNames() (client.NamespaceNames, error) {
return nil, nil return nil, nil
} }
func (m mockConnection) IsValidNamespace(string) bool { func (mockConnection) IsValidNamespace(string) bool {
return true return true
} }
func (m mockConnection) ServerVersion() (*version.Info, error) { func (mockConnection) ServerVersion() (*version.Info, error) {
return nil, nil return nil, nil
} }
func (m mockConnection) CheckConnectivity() bool { func (mockConnection) CheckConnectivity() bool {
return false return false
} }
func (m mockConnection) ActiveContext() string { func (m mockConnection) ActiveContext() string {
return m.ct return m.ct
} }
func (m mockConnection) ActiveNamespace() string { func (mockConnection) ActiveNamespace() string {
return "" return ""
} }
func (m mockConnection) IsActiveNamespace(string) bool { func (mockConnection) IsActiveNamespace(string) bool {
return false return false
} }

View File

@ -114,16 +114,16 @@ func (p *Plugins) load(path string) error {
if err := yaml.Unmarshal(bb, &oo); err != nil { if err := yaml.Unmarshal(bb, &oo); err != nil {
return fmt.Errorf("plugin unmarshal failed for %s: %w", path, err) return fmt.Errorf("plugin unmarshal failed for %s: %w", path, err)
} }
for k, v := range oo.Plugins { for k := range oo.Plugins {
p.Plugins[k] = v p.Plugins[k] = oo.Plugins[k]
} }
case json.PluginMultiSchema: case json.PluginMultiSchema:
var oo plugins var oo plugins
if err := yaml.Unmarshal(bb, &oo); err != nil { if err := yaml.Unmarshal(bb, &oo); err != nil {
return fmt.Errorf("plugin unmarshal failed for %s: %w", path, err) return fmt.Errorf("plugin unmarshal failed for %s: %w", path, err)
} }
for k, v := range oo { for k := range oo {
p.Plugins[k] = v p.Plugins[k] = oo[k]
} }
} }

View File

@ -7,6 +7,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestPluginLoad(t *testing.T) { func TestPluginLoad(t *testing.T) {
@ -103,10 +104,10 @@ func TestSinglePluginFileLoad(t *testing.T) {
} }
p := NewPlugins() p := NewPlugins()
assert.NoError(t, p.load("testdata/plugins/plugins.yaml")) require.NoError(t, p.load("testdata/plugins/plugins.yaml"))
assert.NoError(t, p.loadDir("/random/dir/not/exist")) require.NoError(t, p.loadDir("/random/dir/not/exist"))
assert.Equal(t, 1, len(p.Plugins)) assert.Len(t, p.Plugins, 1)
v, ok := p.Plugins["blah"] v, ok := p.Plugins["blah"]
assert.True(t, ok) assert.True(t, ok)
@ -169,8 +170,8 @@ func TestMultiplePluginFilesLoad(t *testing.T) {
for k, u := range uu { for k, u := range uu {
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
p := NewPlugins() p := NewPlugins()
assert.NoError(t, p.load(u.path)) require.NoError(t, p.load(u.path))
assert.NoError(t, p.loadDir(u.dir)) require.NoError(t, p.loadDir(u.dir))
assert.Equal(t, u.ee, p) assert.Equal(t, u.ee, p)
}) })
} }

View File

@ -26,8 +26,8 @@ type ShellPod struct {
} }
// NewShellPod returns a new instance. // NewShellPod returns a new instance.
func NewShellPod() ShellPod { func NewShellPod() *ShellPod {
return ShellPod{ return &ShellPod{
Image: defaultDockerShellImage, Image: defaultDockerShellImage,
Namespace: "default", Namespace: "default",
Limits: defaultLimits(), Limits: defaultLimits(),
@ -35,15 +35,13 @@ func NewShellPod() ShellPod {
} }
// Validate validates the configuration. // Validate validates the configuration.
func (s ShellPod) Validate() ShellPod { func (s *ShellPod) Validate() {
if s.Image == "" { if s.Image == "" {
s.Image = defaultDockerShellImage s.Image = defaultDockerShellImage
} }
if len(s.Limits) == 0 { if len(s.Limits) == 0 {
s.Limits = defaultLimits() s.Limits = defaultLimits()
} }
return s
} }
func defaultLimits() Limits { func defaultLimits() Limits {

View File

@ -20,14 +20,21 @@ type StyleListener interface {
StylesChanged(*Styles) StylesChanged(*Styles)
} }
// TextStyle tracks text styles.
type TextStyle string type TextStyle string
const ( const (
// TextStyleNormal is the default text style.
TextStyleNormal TextStyle = "normal" TextStyleNormal TextStyle = "normal"
TextStyleBold TextStyle = "bold"
TextStyleDim TextStyle = "dim" // TextStyleBold is the bold text style.
TextStyleBold TextStyle = "bold"
// TextStyleDim is the dim text style.
TextStyleDim TextStyle = "dim"
) )
// ToShortString returns a short string representation of the text style.
func (ts TextStyle) ToShortString() string { func (ts TextStyle) ToShortString() string {
switch ts { switch ts {
case TextStyleNormal: case TextStyleNormal:
@ -283,8 +290,8 @@ func newCharts() Charts {
DefaultDialColors: Colors{Color("palegreen"), Color("orangered")}, DefaultDialColors: Colors{Color("palegreen"), Color("orangered")},
DefaultChartColors: Colors{Color("palegreen"), Color("orangered")}, DefaultChartColors: Colors{Color("palegreen"), Color("orangered")},
ResourceColors: map[string]Colors{ ResourceColors: map[string]Colors{
"cpu": {Color("dodgerblue"), Color("darkslateblue")}, CPU: {Color("dodgerblue"), Color("darkslateblue")},
"mem": {Color("yellow"), Color("goldenrod")}, MEM: {Color("yellow"), Color("goldenrod")},
}, },
} }
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/derailed/tcell/v2" "github.com/derailed/tcell/v2"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestNewStyle(t *testing.T) { func TestNewStyle(t *testing.T) {
@ -38,7 +39,7 @@ func TestColor(t *testing.T) {
func TestSkinHappy(t *testing.T) { func TestSkinHappy(t *testing.T) {
s := config.NewStyles() s := config.NewStyles()
assert.Nil(t, s.Load("../../skins/black-and-wtf.yaml")) require.NoError(t, s.Load("../../skins/black-and-wtf.yaml"))
s.Update() s.Update()
assert.Equal(t, "#ffffff", s.Body().FgColor.String()) assert.Equal(t, "#ffffff", s.Body().FgColor.String())

View File

@ -55,14 +55,14 @@ type Threshold map[string]*Severity
// NewThreshold returns a new threshold. // NewThreshold returns a new threshold.
func NewThreshold() Threshold { func NewThreshold() Threshold {
return Threshold{ return Threshold{
"cpu": NewSeverity(), CPU: NewSeverity(),
"memory": NewSeverity(), MEM: NewSeverity(),
} }
} }
// Validate a namespace is setup correctly. // Validate a namespace is setup correctly.
func (t Threshold) Validate() Threshold { func (t Threshold) Validate() Threshold {
for _, k := range []string{"cpu", "memory"} { for _, k := range []string{CPU, MEM} {
v, ok := t[k] v, ok := t[k]
if !ok { if !ok {
t[k] = NewSeverity() t[k] = NewSeverity()
@ -92,7 +92,7 @@ func (t Threshold) LevelFor(k string, v int) SeverityLevel {
// SeverityColor returns a defcon level associated level. // SeverityColor returns a defcon level associated level.
func (t *Threshold) SeverityColor(k string, v int) string { func (t *Threshold) SeverityColor(k string, v int) string {
// nolint:exhaustive //nolint:exhaustive
switch t.LevelFor(k, v) { switch t.LevelFor(k, v) {
case SeverityHigh: case SeverityHigh:
return "red" return "red"

View File

@ -48,32 +48,37 @@ func TestLevelFor(t *testing.T) {
e config.SeverityLevel e config.SeverityLevel
}{ }{
"normal": { "normal": {
k: "cpu", k: config.CPU,
v: 0, v: 0,
e: config.SeverityLow, e: config.SeverityLow,
}, },
"4": { "4": {
k: "cpu", k: config.CPU,
v: 71, v: 71,
e: config.SeverityMedium, e: config.SeverityMedium,
}, },
"3": { "3": {
k: "cpu", k: config.CPU,
v: 75, v: 75,
e: config.SeverityMedium, e: config.SeverityMedium,
}, },
"2": { "2": {
k: "cpu", k: config.CPU,
v: 80, v: 80,
e: config.SeverityMedium, e: config.SeverityMedium,
}, },
"1": { "1": {
k: "cpu", k: config.CPU,
v: 100, v: 100,
e: config.SeverityHigh, e: config.SeverityHigh,
}, },
"over": { "over": {
k: "cpu", k: config.CPU,
v: 150,
e: config.SeverityLow,
},
"over-mem": {
k: config.MEM,
v: 150, v: 150,
e: config.SeverityLow, e: config.SeverityLow,
}, },

View File

@ -6,6 +6,12 @@ package config
const ( const (
defaultRefreshRate = 2 defaultRefreshRate = 2
defaultMaxConnRetry = 5 defaultMaxConnRetry = 5
// CPU tracks cpu usage.
CPU = "cpu"
// MEM tracks memory usage.
MEM = "memory"
) )
// UI tracks ui specific configs. // UI tracks ui specific configs.

View File

@ -45,7 +45,7 @@ func (v *ViewSetting) IsBlank() bool {
return v == nil || (len(v.Columns) == 0 && v.SortColumn == "") return v == nil || (len(v.Columns) == 0 && v.SortColumn == "")
} }
func (v *ViewSetting) SortCol() (string, bool, error) { func (v *ViewSetting) SortCol() (name string, asc bool, err error) {
if v == nil || v.SortColumn == "" { if v == nil || v.SortColumn == "" {
return "", false, fmt.Errorf("no sort column specified") return "", false, fmt.Errorf("no sort column specified")
} }
@ -180,9 +180,7 @@ func (v *CustomView) getVS(gvr, ns string) *ViewSetting {
} }
k := gvr k := gvr
kk := slices.Collect(maps.Keys(v.Views)) kk := slices.Collect(maps.Keys(v.Views))
slices.SortFunc(kk, func(s1, s2 string) int { slices.SortFunc(kk, strings.Compare)
return strings.Compare(s1, s2)
})
slices.Reverse(kk) slices.Reverse(kk)
for _, key := range kk { for _, key := range kk {
if !strings.HasPrefix(key, gvr) && !strings.HasPrefix(gvr, key) { if !strings.HasPrefix(key, gvr) && !strings.HasPrefix(gvr, key) {
@ -219,7 +217,6 @@ func (v *CustomView) getVS(gvr, ns string) *ViewSetting {
vs := v.Views[key] vs := v.Views[key]
return &vs return &vs
} }
} }
return nil return nil

View File

@ -6,7 +6,9 @@ package config
import ( import (
"testing" "testing"
"github.com/derailed/k9s/internal/client"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestCustomView_getVS(t *testing.T) { func TestCustomView_getVS(t *testing.T) {
@ -22,14 +24,14 @@ func TestCustomView_getVS(t *testing.T) {
}, },
"gvr": { "gvr": {
gvr: "v1/pods", gvr: client.PodGVR.String(),
e: &ViewSetting{ e: &ViewSetting{
Columns: []string{"NAMESPACE", "NAME", "AGE", "IP"}, Columns: []string{"NAMESPACE", "NAME", "AGE", "IP"},
}, },
}, },
"gvr+ns": { "gvr+ns": {
gvr: "v1/pods", gvr: client.PodGVR.String(),
ns: "default", ns: "default",
e: &ViewSetting{ e: &ViewSetting{
Columns: []string{"NAME", "IP", "AGE"}, Columns: []string{"NAME", "IP", "AGE"},
@ -37,7 +39,7 @@ func TestCustomView_getVS(t *testing.T) {
}, },
"rx": { "rx": {
gvr: "v1/pods", gvr: client.PodGVR.String(),
ns: "ns-fred", ns: "ns-fred",
e: &ViewSetting{ e: &ViewSetting{
Columns: []string{"AGE", "NAME", "IP"}, Columns: []string{"AGE", "NAME", "IP"},
@ -52,7 +54,7 @@ func TestCustomView_getVS(t *testing.T) {
}, },
"toast-no-ns": { "toast-no-ns": {
gvr: "v1/pods", gvr: client.PodGVR.String(),
ns: "zorg", ns: "zorg",
e: &ViewSetting{ e: &ViewSetting{
Columns: []string{"NAMESPACE", "NAME", "AGE", "IP"}, Columns: []string{"NAMESPACE", "NAME", "AGE", "IP"},
@ -60,13 +62,13 @@ func TestCustomView_getVS(t *testing.T) {
}, },
"toast-no-res": { "toast-no-res": {
gvr: "v1/services", gvr: client.SvcGVR.String(),
ns: "zorg", ns: "zorg",
}, },
} }
v := NewCustomView() v := NewCustomView()
assert.NoError(t, v.Load("testdata/views/views.yaml")) require.NoError(t, v.Load("testdata/views/views.yaml"))
for k, u := range uu { for k, u := range uu {
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, v.getVS(u.gvr, u.ns)) assert.Equal(t, u.e, v.getVS(u.gvr, u.ns))

View File

@ -7,8 +7,10 @@ import (
"log/slog" "log/slog"
"testing" "testing"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func init() { func init() {
@ -26,7 +28,7 @@ func TestCustomViewLoad(t *testing.T) {
"gvr": { "gvr": {
path: "testdata/views/views.yaml", path: "testdata/views/views.yaml",
key: "v1/pods", key: client.PodGVR.String(),
e: []string{"NAMESPACE", "NAME", "AGE", "IP"}, e: []string{"NAMESPACE", "NAME", "AGE", "IP"},
}, },
@ -41,7 +43,7 @@ func TestCustomViewLoad(t *testing.T) {
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
cfg := config.NewCustomView() cfg := config.NewCustomView()
assert.NoError(t, cfg.Load(u.path)) require.NoError(t, cfg.Load(u.path))
assert.Equal(t, u.e, cfg.Views[u.key].Columns) assert.Equal(t, u.e, cfg.Views[u.key].Columns)
}) })
} }

56
internal/dao/accessor.go Normal file
View File

@ -0,0 +1,56 @@
package dao
import (
"log/slog"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/slogs"
)
var accessors = Accessors{
*client.WkGVR: new(Workload),
*client.CtGVR: new(Context),
*client.CoGVR: new(Container),
*client.ScnGVR: new(ImageScan),
*client.SdGVR: new(ScreenDump),
*client.BeGVR: new(Benchmark),
*client.PfGVR: new(PortForward),
*client.DirGVR: new(Dir),
*client.SvcGVR: new(Service),
*client.PodGVR: new(Pod),
*client.NodeGVR: new(Node),
*client.NsGVR: new(Namespace),
*client.CmGVR: new(ConfigMap),
*client.SecGVR: new(Secret),
*client.DpGVR: new(Deployment),
*client.DsGVR: new(DaemonSet),
*client.StsGVR: new(StatefulSet),
*client.RsGVR: new(ReplicaSet),
*client.CjGVR: new(CronJob),
*client.JobGVR: new(Job),
*client.HmGVR: new(HelmChart),
*client.HmhGVR: new(HelmHistory),
*client.CrdGVR: new(CustomResourceDefinition),
}
// Accessors represents a collection of dao accessors.
type Accessors map[client.GVR]Accessor
// AccessorFor returns a client accessor for a resource if registered.
// Otherwise it returns a generic accessor.
// Customize here for non resource types or types with metrics or logs.
func AccessorFor(f Factory, gvr *client.GVR) (Accessor, error) {
r, ok := accessors[*gvr]
if !ok {
r = new(Scaler)
slog.Debug("No DAO registry entry. Using generics!", slogs.GVR, gvr)
}
r.Init(f, gvr)
return r, nil
}

View File

@ -14,8 +14,8 @@ import (
"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/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/view/cmd"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
) )
var _ Accessor = (*Alias)(nil) var _ Accessor = (*Alias)(nil)
@ -32,22 +32,18 @@ func NewAlias(f Factory) *Alias {
a := Alias{ a := Alias{
Aliases: config.NewAliases(), Aliases: config.NewAliases(),
} }
a.Init(f, client.NewGVR("aliases")) a.Init(f, client.AliGVR)
return &a return &a
} }
func (a *Alias) AliasesFor(s string) []string { // AliasesFor returns a set of aliases for a given gvr.
return a.Aliases.AliasesFor(s) func (a *Alias) AliasesFor(gvr *client.GVR) sets.Set[string] {
} return a.Aliases.AliasesFor(gvr)
// Check verifies an alias is defined for this command.
func (a *Alias) Check(cmd string) (string, bool) {
return a.Aliases.Get(cmd)
} }
// List returns a collection of aliases. // List returns a collection of aliases.
func (a *Alias) List(ctx context.Context, _ string) ([]runtime.Object, error) { func (*Alias) List(ctx context.Context, _ string) ([]runtime.Object, error) {
aa, ok := ctx.Value(internal.KeyAliases).(*Alias) aa, ok := ctx.Value(internal.KeyAliases).(*Alias)
if !ok { if !ok {
return nil, fmt.Errorf("expecting *Alias but got %T", ctx.Value(internal.KeyAliases)) return nil, fmt.Errorf("expecting *Alias but got %T", ctx.Value(internal.KeyAliases))
@ -66,24 +62,19 @@ func (a *Alias) List(ctx context.Context, _ string) ([]runtime.Object, error) {
} }
// AsGVR returns a matching gvr if it exists. // AsGVR returns a matching gvr if it exists.
func (a *Alias) AsGVR(c string) (client.GVR, string, bool) { func (a *Alias) AsGVR(alias string) (*client.GVR, string, bool) {
exp, ok := a.Aliases.Get(c) gvr, ok := a.Aliases.Get(alias)
if !ok { if ok {
return client.NoGVR, "", ok if pgvr := MetaAccess.Lookup(alias); pgvr != client.NoGVR {
} return pgvr, "", ok
p := cmd.NewInterpreter(exp) }
if strings.Contains(p.Cmd(), "/") {
return client.NewGVR(p.Cmd()), "", true
}
if gvr, ok := a.Aliases.Get(p.Cmd()); ok {
return client.NewGVR(gvr), exp, true
} }
return client.NoGVR, "", false return gvr, "", ok
} }
// Get fetch a resource. // Get fetch a resource.
func (a *Alias) Get(_ context.Context, _ string) (runtime.Object, error) { func (*Alias) Get(_ context.Context, _ string) (runtime.Object, error) {
return nil, errors.New("nyi") return nil, errors.New("nyi")
} }
@ -109,25 +100,21 @@ func (a *Alias) load(path string) error {
if IsK9sMeta(meta) { if IsK9sMeta(meta) {
continue continue
} }
gvrStr := gvr.String()
if IsCRD(meta) { if IsCRD(meta) {
crdGVRS = append(crdGVRS, gvr) crdGVRS = append(crdGVRS, gvr)
continue continue
} }
a.Define(gvr, gvr.AsResourceName())
a.Define(gvrStr, gvr.AsResourceName())
// Allow single shot commands for k8s resources only! // Allow single shot commands for k8s resources only!
if isStandardGroup(gvr.GVSub()) { if isStandardGroup(gvr.GVSub()) {
a.Define(gvrStr, strings.ToLower(meta.Kind), meta.Name) a.Define(gvr, meta.Name)
a.Define(gvrStr, meta.SingularName) a.Define(gvr, meta.SingularName)
} }
if len(meta.ShortNames) > 0 { if len(meta.ShortNames) > 0 {
a.Define(gvrStr, meta.ShortNames...) a.Define(gvr, meta.ShortNames...)
} }
a.Define(gvrStr, gvrStr) a.Define(gvr, gvr.String())
} }
for _, gvr := range crdGVRS { for _, gvr := range crdGVRS {
@ -135,15 +122,14 @@ func (a *Alias) load(path string) error {
if err != nil { if err != nil {
return err return err
} }
gvrStr := gvr.String() a.Define(gvr, strings.ToLower(meta.Kind), meta.Name)
a.Define(gvrStr, strings.ToLower(meta.Kind), meta.Name) a.Define(gvr, meta.SingularName)
a.Define(gvrStr, meta.SingularName)
if len(meta.ShortNames) > 0 { if len(meta.ShortNames) > 0 {
a.Define(gvrStr, meta.ShortNames...) a.Define(gvr, meta.ShortNames...)
} }
a.Define(gvrStr, gvrStr) a.Define(gvr, gvr.String())
a.Define(gvrStr, meta.Name+"."+meta.Group) a.Define(gvr, meta.Name+"."+meta.Group)
} }
return nil return nil

View File

@ -13,27 +13,28 @@ import (
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestAsGVR(t *testing.T) { func TestAsGVR(t *testing.T) {
a := dao.NewAlias(makeFactory()) a := dao.NewAlias(makeFactory())
a.Define("v1/pods", "po", "pod", "pods") a.Define(client.PodGVR, "po", "pod", "pods")
a.Define("workloads", "workloads", "workload", "wkl") a.Define(client.WkGVR, client.WkGVR.String(), "workload", "wkl")
uu := map[string]struct { uu := map[string]struct {
cmd string cmd string
ok bool ok bool
gvr client.GVR gvr *client.GVR
}{ }{
"ok": { "ok": {
cmd: "pods", cmd: "pods",
ok: true, ok: true,
gvr: client.NewGVR("v1/pods"), gvr: client.PodGVR,
}, },
"ok-short": { "ok-short": {
cmd: "po", cmd: "po",
ok: true, ok: true,
gvr: client.NewGVR("v1/pods"), gvr: client.PodGVR,
}, },
"missing": { "missing": {
cmd: "zorg", cmd: "zorg",
@ -41,7 +42,7 @@ func TestAsGVR(t *testing.T) {
"alias": { "alias": {
cmd: "wkl", cmd: "wkl",
ok: true, ok: true,
gvr: client.NewGVR("workloads"), gvr: client.WkGVR,
}, },
} }
@ -59,27 +60,30 @@ func TestAsGVR(t *testing.T) {
func TestAliasList(t *testing.T) { func TestAliasList(t *testing.T) {
a := dao.Alias{} a := dao.Alias{}
a.Init(makeFactory(), client.NewGVR("aliases")) a.Init(makeFactory(), client.AliGVR)
ctx := context.WithValue(context.Background(), internal.KeyAliases, makeAliases()) ctx := context.WithValue(context.Background(), internal.KeyAliases, makeAliases())
oo, err := a.List(ctx, "-") oo, err := a.List(ctx, "-")
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, 2, len(oo)) assert.Len(t, oo, 2)
assert.Equal(t, 2, len(oo[0].(render.AliasRes).Aliases)) assert.Len(t, oo[0].(render.AliasRes).Aliases, 2)
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
func makeAliases() *dao.Alias { func makeAliases() *dao.Alias {
gvr1 := client.NewGVR("v1/fred")
gvr2 := client.NewGVR("v1/blee")
return &dao.Alias{ return &dao.Alias{
Aliases: &config.Aliases{ Aliases: &config.Aliases{
Alias: config.Alias{ Alias: config.Alias{
"fred": "v1/fred", "fred": gvr1,
"f": "v1/fred", "f": gvr1,
"blee": "v1/blee", "blee": gvr2,
"b": "v1/blee", "b": gvr2,
}, },
}, },
} }

View File

@ -30,17 +30,17 @@ type Benchmark struct {
} }
// Delete nukes a resource. // Delete nukes a resource.
func (b *Benchmark) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error { func (*Benchmark) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error {
return os.Remove(path) return os.Remove(path)
} }
// Get returns a resource. // Get returns a resource.
func (b *Benchmark) Get(context.Context, string) (runtime.Object, error) { func (*Benchmark) Get(context.Context, string) (runtime.Object, error) {
panic("NYI") panic("NYI")
} }
// List returns a collection of resources. // List returns a collection of resources.
func (b *Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error) { func (*Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error) {
dir, ok := ctx.Value(internal.KeyDir).(string) dir, ok := ctx.Value(internal.KeyDir).(string)
if !ok { if !ok {
return nil, errors.New("no benchmark dir found in context") return nil, errors.New("no benchmark dir found in context")

View File

@ -12,17 +12,18 @@ import (
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestBenchmarkList(t *testing.T) { func TestBenchmarkList(t *testing.T) {
a := dao.Benchmark{} a := dao.Benchmark{}
a.Init(makeFactory(), client.NewGVR("benchmarks")) a.Init(makeFactory(), client.BeGVR)
ctx := context.WithValue(context.Background(), internal.KeyDir, "testdata/bench") ctx := context.WithValue(context.Background(), internal.KeyDir, "testdata/bench")
ctx = context.WithValue(ctx, internal.KeyPath, "") ctx = context.WithValue(ctx, internal.KeyPath, "")
oo, err := a.List(ctx, "-") oo, err := a.List(ctx, "-")
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, 1, len(oo)) assert.Len(t, oo, 1)
assert.Equal(t, "testdata/bench/default_fred_1577308050814961000.txt", oo[0].(render.BenchInfo).Path) assert.Equal(t, "testdata/bench/default_fred_1577308050814961000.txt", oo[0].(render.BenchInfo).Path)
} }

View File

@ -9,7 +9,6 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"sync" "sync"
"time"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
@ -19,9 +18,12 @@ import (
// RefScanner represents a resource reference scanner. // RefScanner represents a resource reference scanner.
type RefScanner interface { type RefScanner interface {
// Init initializes the scanner // Init initializes the scanner
Init(Factory, client.GVR) Init(Factory, *client.GVR)
// Scan scan the resource for references. // Scan scan the resource for references.
Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) Scan(ctx context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error)
// ScanSA scan the resource for serviceaccount references.
ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error)
} }
@ -40,27 +42,21 @@ var (
_ RefScanner = (*DaemonSet)(nil) _ RefScanner = (*DaemonSet)(nil)
_ RefScanner = (*Job)(nil) _ RefScanner = (*Job)(nil)
_ RefScanner = (*CronJob)(nil) _ RefScanner = (*CronJob)(nil)
// _ RefScanner = (*Pod)(nil)
) )
func scanners() map[string]RefScanner { func scanners() map[*client.GVR]RefScanner {
return map[string]RefScanner{ return map[*client.GVR]RefScanner{
"apps/v1/deployments": &Deployment{}, client.DpGVR: new(Deployment),
"apps/v1/statefulsets": &StatefulSet{}, client.DsGVR: new(DaemonSet),
"apps/v1/daemonsets": &DaemonSet{}, client.StsGVR: new(StatefulSet),
"batch/v1/jobs": &Job{}, client.CjGVR: new(CronJob),
"batch/v1/cronjobs": &CronJob{}, client.JobGVR: new(Job),
// "v1/pods": &Pod{},
} }
} }
// ScanForRefs scans cluster resources for resource references. // ScanForRefs scans cluster resources for resource references.
func ScanForRefs(ctx context.Context, f Factory) (Refs, error) { func ScanForRefs(ctx context.Context, f Factory) (Refs, error) {
defer func(t time.Time) { rgvr, ok := ctx.Value(internal.KeyGVR).(*client.GVR)
slog.Debug("Cluster Scan", slogs.Elapsed, time.Since(t))
}(time.Now())
gvr, ok := ctx.Value(internal.KeyGVR).(client.GVR)
if !ok { if !ok {
return nil, errors.New("expecting context GVR") return nil, errors.New("expecting context GVR")
} }
@ -73,15 +69,14 @@ func ScanForRefs(ctx context.Context, f Factory) (Refs, error) {
slog.Warn("Expecting context Wait key. Using default") slog.Warn("Expecting context Wait key. Using default")
} }
ss := scanners()
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(len(ss))
out := make(chan Refs) out := make(chan Refs)
for k, s := range ss { for gvr, scanner := range scanners() {
go func(ctx context.Context, kind string, s RefScanner, out chan Refs, wait bool) { wg.Add(1)
go func(ctx context.Context, gvr *client.GVR, s RefScanner, out chan Refs, wait bool) {
defer wg.Done() defer wg.Done()
s.Init(f, client.NewGVR(kind)) s.Init(f, gvr)
refs, err := s.Scan(ctx, gvr, fqn, wait) refs, err := s.Scan(ctx, rgvr, fqn, wait)
if err != nil { if err != nil {
slog.Error("Reference scan failed for", slog.Error("Reference scan failed for",
slogs.RefType, fmt.Sprintf("%T", s), slogs.RefType, fmt.Sprintf("%T", s),
@ -94,7 +89,7 @@ func ScanForRefs(ctx context.Context, f Factory) (Refs, error) {
case <-ctx.Done(): case <-ctx.Done():
return return
} }
}(ctx, k, s, out, wait) }(ctx, gvr, scanner, out, wait)
} }
go func() { go func() {
@ -112,10 +107,6 @@ func ScanForRefs(ctx context.Context, f Factory) (Refs, error) {
// ScanForSARefs scans cluster resources for serviceaccount refs. // ScanForSARefs scans cluster resources for serviceaccount refs.
func ScanForSARefs(ctx context.Context, f Factory) (Refs, error) { func ScanForSARefs(ctx context.Context, f Factory) (Refs, error) {
defer func(t time.Time) {
slog.Debug("Time to scan Cluster SA", slogs.Elapsed, time.Since(t))
}(time.Now())
fqn, ok := ctx.Value(internal.KeyPath).(string) fqn, ok := ctx.Value(internal.KeyPath).(string)
if !ok { if !ok {
return nil, errors.New("expecting context Path") return nil, errors.New("expecting context Path")
@ -125,14 +116,13 @@ func ScanForSARefs(ctx context.Context, f Factory) (Refs, error) {
return nil, errors.New("expecting context Wait") return nil, errors.New("expecting context Wait")
} }
ss := scanners()
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(len(ss))
out := make(chan Refs) out := make(chan Refs)
for k, s := range ss { for gvr, scanner := range scanners() {
go func(ctx context.Context, kind string, s RefScanner, out chan Refs, wait bool) { wg.Add(1)
go func(ctx context.Context, gvr *client.GVR, s RefScanner, out chan Refs, wait bool) {
defer wg.Done() defer wg.Done()
s.Init(f, client.NewGVR(kind)) s.Init(f, gvr)
refs, err := s.ScanSA(ctx, fqn, wait) refs, err := s.ScanSA(ctx, fqn, wait)
if err != nil { if err != nil {
slog.Error("ServiceAccount scan failed", slog.Error("ServiceAccount scan failed",
@ -146,7 +136,7 @@ func ScanForSARefs(ctx context.Context, f Factory) (Refs, error) {
case <-ctx.Done(): case <-ctx.Done():
return return
} }
}(ctx, k, s, out, wait) }(ctx, gvr, scanner, out, wait)
} }
go func() { go func() {

View File

@ -54,14 +54,33 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
return nil, err return nil, err
} }
res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers)+len(po.Spec.EphemeralContainers)) res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers)+len(po.Spec.EphemeralContainers))
for i, co := range po.Spec.InitContainers { for i := range po.Spec.InitContainers {
res = append(res, makeContainerRes(initIDX, i, co, po, cmx[co.Name])) res = append(res, makeContainerRes(
initIDX,
i,
&(po.Spec.InitContainers[i]),
po,
cmx[po.Spec.InitContainers[i].Name]),
)
} }
for i, co := range po.Spec.Containers { for i := range po.Spec.Containers {
res = append(res, makeContainerRes(mainIDX, i, co, po, cmx[co.Name])) res = append(res, makeContainerRes(
mainIDX,
i,
&(po.Spec.Containers[i]),
po,
cmx[po.Spec.Containers[i].Name]),
)
} }
for i, co := range po.Spec.EphemeralContainers { for i := range po.Spec.EphemeralContainers {
res = append(res, makeContainerRes(ephIDX, i, v1.Container(co.EphemeralContainerCommon), po, cmx[co.Name])) co := v1.Container(po.Spec.EphemeralContainers[i].EphemeralContainerCommon)
res = append(res, makeContainerRes(
ephIDX,
i,
&co,
po,
cmx[co.Name]),
)
} }
return res, nil return res, nil
@ -70,7 +89,7 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
// TailLogs tails a given container logs. // TailLogs tails a given container logs.
func (c *Container) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) { func (c *Container) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {
po := Pod{} po := Pod{}
po.Init(c.Factory, client.NewGVR("v1/pods")) po.Init(c.Factory, client.PodGVR)
return po.TailLogs(ctx, opts) return po.TailLogs(ctx, opts)
} }
@ -78,34 +97,34 @@ func (c *Container) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan,
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
func makeContainerRes(kind string, idx int, co v1.Container, po *v1.Pod, cmx *mv1beta1.ContainerMetrics) render.ContainerRes { func makeContainerRes(kind string, idx int, co *v1.Container, po *v1.Pod, cmx *mv1beta1.ContainerMetrics) render.ContainerRes {
return render.ContainerRes{ return render.ContainerRes{
Idx: kind + strconv.Itoa(idx+1), Idx: kind + strconv.Itoa(idx+1),
Container: &co, Container: co,
Status: getContainerStatus(kind, co.Name, po.Status), Status: getContainerStatus(kind, co.Name, &po.Status),
MX: cmx, MX: cmx,
Age: po.GetCreationTimestamp(), Age: po.GetCreationTimestamp(),
} }
} }
func getContainerStatus(kind string, name string, status v1.PodStatus) *v1.ContainerStatus { func getContainerStatus(kind, name string, status *v1.PodStatus) *v1.ContainerStatus {
switch kind { switch kind {
case mainIDX: case mainIDX:
for _, s := range status.ContainerStatuses { for i := range status.ContainerStatuses {
if s.Name == name { if status.ContainerStatuses[i].Name == name {
return &s return &status.ContainerStatuses[i]
} }
} }
case initIDX: case initIDX:
for _, s := range status.InitContainerStatuses { for i := range status.InitContainerStatuses {
if s.Name == name { if status.InitContainerStatuses[i].Name == name {
return &s return &status.InitContainerStatuses[i]
} }
} }
case ephIDX: case ephIDX:
for _, s := range status.EphemeralContainerStatuses { for i := range status.EphemeralContainerStatuses {
if s.Name == name { if status.EphemeralContainerStatuses[i].Name == name {
return &s return &status.EphemeralContainerStatuses[i]
} }
} }
} }
@ -114,7 +133,7 @@ func getContainerStatus(kind string, name string, status v1.PodStatus) *v1.Conta
} }
func (c *Container) fetchPod(fqn string) (*v1.Pod, error) { func (c *Container) fetchPod(fqn string) (*v1.Pod, error) {
o, err := c.getFactory().Get("v1/pods", fqn, true, labels.Everything()) o, err := c.getFactory().Get(client.PodGVR, fqn, true, labels.Everything())
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to locate pod %q: %w", fqn, err) return nil, fmt.Errorf("failed to locate pod %q: %w", fqn, err)
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/watch" "github.com/derailed/k9s/internal/watch"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1" 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/labels" "k8s.io/apimachinery/pkg/labels"
@ -28,12 +29,12 @@ import (
func TestContainerList(t *testing.T) { func TestContainerList(t *testing.T) {
c := dao.Container{} c := dao.Container{}
c.Init(makePodFactory(), client.NewGVR("containers")) c.Init(makePodFactory(), client.CoGVR)
ctx := context.WithValue(context.Background(), internal.KeyPath, "fred/p1") ctx := context.WithValue(context.Background(), internal.KeyPath, "fred/p1")
oo, err := c.List(ctx, "") oo, err := c.List(ctx, "")
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, 1, len(oo)) assert.Len(t, oo, 1)
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -45,58 +46,58 @@ func makeConn() *conn {
return &conn{} return &conn{}
} }
func (c *conn) Config() *client.Config { return nil } func (*conn) Config() *client.Config { return nil }
func (c *conn) Dial() (kubernetes.Interface, error) { return nil, nil } func (*conn) Dial() (kubernetes.Interface, error) { return nil, nil }
func (c *conn) DialLogs() (kubernetes.Interface, error) { return nil, nil } func (*conn) DialLogs() (kubernetes.Interface, error) { return nil, nil }
func (c *conn) ConnectionOK() bool { return true } func (*conn) ConnectionOK() bool { return true }
func (c *conn) SwitchContext(ctx string) error { return nil } func (*conn) SwitchContext(string) error { return nil }
func (c *conn) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { return nil, nil } func (*conn) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { return nil, nil }
func (c *conn) RestConfig() (*restclient.Config, error) { return nil, nil } func (*conn) RestConfig() (*restclient.Config, error) { return nil, nil }
func (c *conn) MXDial() (*versioned.Clientset, error) { return nil, nil } func (*conn) MXDial() (*versioned.Clientset, error) { return nil, nil }
func (c *conn) DynDial() (dynamic.Interface, error) { return nil, nil } func (*conn) DynDial() (dynamic.Interface, error) { return nil, nil }
func (c *conn) HasMetrics() bool { return false } func (*conn) HasMetrics() bool { return false }
func (c *conn) CheckConnectivity() bool { return false } func (*conn) CheckConnectivity() bool { return false }
func (c *conn) IsNamespaced(n string) bool { return false } func (*conn) IsNamespaced(string) bool { return false }
func (c *conn) SupportsResource(group string) bool { return false } func (*conn) SupportsResource(string) bool { return false }
func (c *conn) ValidNamespaces() ([]v1.Namespace, error) { return nil, nil } func (*conn) ValidNamespaces() ([]v1.Namespace, error) { return nil, nil }
func (c *conn) SupportsRes(grp string, versions []string) (string, bool, error) { func (*conn) SupportsRes(string, []string) (a string, b bool, e error) { return "", false, nil }
return "", false, nil func (*conn) ServerVersion() (*version.Info, error) { return nil, nil }
} func (*conn) CurrentNamespaceName() (string, error) { return "", nil }
func (c *conn) ServerVersion() (*version.Info, error) { return nil, nil } func (*conn) CanI(string, *client.GVR, string, []string) (bool, error) { return true, nil }
func (c *conn) CurrentNamespaceName() (string, error) { return "", nil } func (*conn) ActiveContext() string { return "" }
func (c *conn) CanI(ns, gvr, n string, verbs []string) (bool, error) { return true, nil } func (*conn) ActiveNamespace() string { return "" }
func (c *conn) ActiveContext() string { return "" } func (*conn) IsValidNamespace(string) bool { return true }
func (c *conn) ActiveNamespace() string { return "" } func (*conn) ValidNamespaceNames() (client.NamespaceNames, error) { return nil, nil }
func (c *conn) IsValidNamespace(string) bool { return true } func (*conn) IsActiveNamespace(string) bool { return false }
func (c *conn) ValidNamespaceNames() (client.NamespaceNames, error) { return nil, nil }
func (c *conn) IsActiveNamespace(string) bool { return false }
type podFactory struct{} type podFactory struct{}
var _ dao.Factory = &testFactory{} var _ dao.Factory = &testFactory{}
func (f podFactory) Client() client.Connection { func (podFactory) Client() client.Connection {
return makeConn() return makeConn()
} }
func (f podFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) { func (podFactory) Get(*client.GVR, string, bool, labels.Selector) (runtime.Object, error) {
var m map[string]interface{} var m map[string]any
if err := yaml.Unmarshal([]byte(poYaml()), &m); err != nil { if err := yaml.Unmarshal([]byte(poYaml()), &m); err != nil {
return nil, err return nil, err
} }
return &unstructured.Unstructured{Object: m}, nil return &unstructured.Unstructured{Object: m}, nil
} }
func (f podFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) { func (podFactory) List(*client.GVR, string, bool, labels.Selector) ([]runtime.Object, error) {
return nil, nil return nil, nil
} }
func (f podFactory) ForResource(ns, gvr string) (informers.GenericInformer, error) { return nil, nil } func (podFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) {
func (f podFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) {
return nil, nil return nil, nil
} }
func (f podFactory) WaitForCacheSync() {} func (podFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) {
func (f podFactory) Forwarders() watch.Forwarders { return nil } return nil, nil
func (f podFactory) DeleteForwarder(string) {} }
func (podFactory) WaitForCacheSync() {}
func (podFactory) Forwarders() watch.Forwarders { return nil }
func (podFactory) DeleteForwarder(string) {}
func makePodFactory() dao.Factory { func makePodFactory() dao.Factory {
return podFactory{} return podFactory{}

View File

@ -28,7 +28,7 @@ func (c *Context) config() *client.Config {
} }
// Get a Context. // Get a Context.
func (c *Context) Get(ctx context.Context, path string) (runtime.Object, error) { func (c *Context) Get(_ context.Context, path string) (runtime.Object, error) {
co, err := c.config().GetContext(path) co, err := c.config().GetContext(path)
if err != nil { if err != nil {
return nil, err return nil, err
@ -37,7 +37,7 @@ func (c *Context) Get(ctx context.Context, path string) (runtime.Object, error)
} }
// List all Contexts on the current cluster. // List all Contexts on the current cluster.
func (c *Context) List(_ context.Context, _ string) ([]runtime.Object, error) { func (c *Context) List(context.Context, string) ([]runtime.Object, error) {
ctxs, err := c.config().Contexts() ctxs, err := c.config().Contexts()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -20,10 +20,7 @@ import (
"k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/util/rand"
) )
const ( const maxJobNameSize = 42
maxJobNameSize = 42
jobGVR = "batch/v1/jobs"
)
var ( var (
_ Accessor = (*CronJob)(nil) _ Accessor = (*CronJob)(nil)
@ -37,7 +34,7 @@ type CronJob struct {
} }
// ListImages lists container images. // ListImages lists container images.
func (c *CronJob) ListImages(ctx context.Context, fqn string) ([]string, error) { func (c *CronJob) ListImages(_ context.Context, fqn string) ([]string, error) {
cj, err := c.GetInstance(fqn) cj, err := c.GetInstance(fqn)
if err != nil { if err != nil {
return nil, err return nil, err
@ -49,7 +46,7 @@ func (c *CronJob) ListImages(ctx context.Context, fqn string) ([]string, error)
// Run a CronJob. // Run a CronJob.
func (c *CronJob) Run(path string) error { func (c *CronJob) Run(path string) error {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := c.Client().CanI(ns, jobGVR, n, []string{client.GetVerb, client.CreateVerb}) auth, err := c.Client().CanI(ns, c.gvr, n, []string{client.GetVerb, client.CreateVerb})
if err != nil { if err != nil {
return err return err
} }
@ -57,7 +54,7 @@ func (c *CronJob) Run(path string) error {
return fmt.Errorf("user is not authorized to run jobs") return fmt.Errorf("user is not authorized to run jobs")
} }
o, err := c.getFactory().Get(c.GVR(), path, true, labels.Everything()) o, err := c.getFactory().Get(c.gvr, path, true, labels.Everything())
if err != nil { if err != nil {
return err return err
} }
@ -70,7 +67,7 @@ func (c *CronJob) Run(path string) error {
if len(cj.Name) >= maxJobNameSize { if len(cj.Name) >= maxJobNameSize {
jobName = cj.Name[0:maxJobNameSize] jobName = cj.Name[0:maxJobNameSize]
} }
true := true trueVal := true
job := &batchv1.Job{ job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: jobName + "-manual-" + rand.String(3), Name: jobName + "-manual-" + rand.String(3),
@ -81,8 +78,8 @@ func (c *CronJob) Run(path string) error {
{ {
APIVersion: c.gvr.GV().String(), APIVersion: c.gvr.GV().String(),
Kind: "CronJob", Kind: "CronJob",
BlockOwnerDeletion: &true, BlockOwnerDeletion: &trueVal,
Controller: &true, Controller: &trueVal,
Name: cj.Name, Name: cj.Name,
UID: cj.UID, UID: cj.UID,
}, },
@ -102,9 +99,9 @@ func (c *CronJob) Run(path string) error {
} }
// ScanSA scans for serviceaccount refs. // ScanSA scans for serviceaccount refs.
func (c *CronJob) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { func (c *CronJob) ScanSA(_ context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn) ns, n := client.Namespaced(fqn)
oo, err := c.getFactory().List(c.GVR(), ns, wait, labels.Everything()) oo, err := c.getFactory().List(c.gvr, ns, wait, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -129,7 +126,7 @@ func (c *CronJob) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, erro
// GetInstance fetch a matching cronjob. // GetInstance fetch a matching cronjob.
func (c *CronJob) GetInstance(fqn string) (*batchv1.CronJob, error) { func (c *CronJob) GetInstance(fqn string) (*batchv1.CronJob, error) {
o, err := c.getFactory().Get(c.GVR(), fqn, true, labels.Everything()) o, err := c.getFactory().Get(c.gvr, fqn, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -146,7 +143,7 @@ func (c *CronJob) GetInstance(fqn string) (*batchv1.CronJob, error) {
// ToggleSuspend toggles suspend/resume on a CronJob. // ToggleSuspend toggles suspend/resume on a CronJob.
func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error { func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := c.Client().CanI(ns, c.GVR(), n, []string{client.GetVerb, client.UpdateVerb}) auth, err := c.Client().CanI(ns, c.gvr, n, []string{client.GetVerb, client.UpdateVerb})
if err != nil { if err != nil {
return err return err
} }
@ -166,8 +163,8 @@ func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error {
current := !*cj.Spec.Suspend current := !*cj.Spec.Suspend
cj.Spec.Suspend = &current cj.Spec.Suspend = &current
} else { } else {
true := true trueVal := true
cj.Spec.Suspend = &true cj.Spec.Suspend = &trueVal
} }
_, err = dial.BatchV1().CronJobs(ns).Update(ctx, cj, metav1.UpdateOptions{}) _, err = dial.BatchV1().CronJobs(ns).Update(ctx, cj, metav1.UpdateOptions{})
@ -175,9 +172,9 @@ func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error {
} }
// Scan scans for cluster resource refs. // Scan scans for cluster resource refs.
func (c *CronJob) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) { func (c *CronJob) Scan(_ context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn) ns, n := client.Namespaced(fqn)
oo, err := c.getFactory().List(c.GVR(), ns, wait, labels.Everything()) oo, err := c.getFactory().List(c.gvr, ns, wait, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -190,7 +187,7 @@ func (c *CronJob) Scan(ctx context.Context, gvr client.GVR, fqn string, wait boo
return nil, errors.New("expecting CronJob resource") return nil, errors.New("expecting CronJob resource")
} }
switch gvr { switch gvr {
case CmGVR: case client.CmGVR:
if !hasConfigMap(&cj.Spec.JobTemplate.Spec.Template.Spec, n) { if !hasConfigMap(&cj.Spec.JobTemplate.Spec.Template.Spec, n) {
continue continue
} }
@ -198,7 +195,7 @@ func (c *CronJob) Scan(ctx context.Context, gvr client.GVR, fqn string, wait boo
GVR: c.GVR(), GVR: c.GVR(),
FQN: client.FQN(cj.Namespace, cj.Name), FQN: client.FQN(cj.Namespace, cj.Name),
}) })
case SecGVR: case client.SecGVR:
found, err := hasSecret(c.Factory, &cj.Spec.JobTemplate.Spec.Template.Spec, cj.Namespace, n, wait) found, err := hasSecret(c.Factory, &cj.Spec.JobTemplate.Spec.Template.Spec, cj.Namespace, n, wait)
if err != nil { if err != nil {
slog.Warn("Failed to locate secret", slog.Warn("Failed to locate secret",
@ -214,7 +211,7 @@ func (c *CronJob) Scan(ctx context.Context, gvr client.GVR, fqn string, wait boo
GVR: c.GVR(), GVR: c.GVR(),
FQN: client.FQN(cj.Namespace, cj.Name), FQN: client.FQN(cj.Namespace, cj.Name),
}) })
case PcGVR: case client.PcGVR:
if !hasPC(&cj.Spec.JobTemplate.Spec.Template.Spec, n) { if !hasPC(&cj.Spec.JobTemplate.Spec.Template.Spec, n) {
continue continue
} }

View File

@ -10,12 +10,12 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
func mustMap(o runtime.Object, field string) map[string]interface{} { func mustMap(o runtime.Object, field string) map[string]any {
u, ok := o.(*unstructured.Unstructured) u, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
panic("no unstructured") panic("no unstructured")
} }
m, ok := u.Object[field].(map[string]interface{}) m, ok := u.Object[field].(map[string]any)
if !ok { if !ok {
panic(fmt.Sprintf("map extract failed for %q", field)) panic(fmt.Sprintf("map extract failed for %q", field))
} }
@ -23,20 +23,20 @@ func mustMap(o runtime.Object, field string) map[string]interface{} {
return m return m
} }
func mustSlice(o runtime.Object, field string) []interface{} { func mustSlice(o runtime.Object, field string) []any {
u, ok := o.(*unstructured.Unstructured) u, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return []interface{}{} return nil
} }
s, ok := u.Object[field].([]interface{}) s, ok := u.Object[field].([]any)
if !ok { if !ok {
return []interface{}{} return nil
} }
return s return s
} }
func mustField(o map[string]interface{}, field string) interface{} { func mustField(o map[string]any, field string) any {
f, ok := o[field] f, ok := o[field]
if !ok { if !ok {
panic(fmt.Sprintf("no field for %q", field)) panic(fmt.Sprintf("no field for %q", field))

View File

@ -10,6 +10,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
) )
@ -24,20 +25,20 @@ func TestCruiserSlice(t *testing.T) {
o := loadJSON(t, "crb") o := loadJSON(t, "crb")
s := mustSlice(o, "subjects") s := mustSlice(o, "subjects")
assert.Equal(t, 1, len(s)) assert.Len(t, s, 1)
assert.Equal(t, "fernand", mustField(s[0].(map[string]interface{}), "name")) assert.Equal(t, "fernand", mustField(s[0].(map[string]any), "name"))
assert.Equal(t, "User", mustField(s[0].(map[string]interface{}), "kind")) assert.Equal(t, "User", mustField(s[0].(map[string]any), "kind"))
} }
// Helpers... // Helpers...
func loadJSON(t assert.TestingT, n string) *unstructured.Unstructured { func loadJSON(t require.TestingT, n string) *unstructured.Unstructured {
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
assert.Nil(t, err) require.NoError(t, err)
var o unstructured.Unstructured var o unstructured.Unstructured
err = json.Unmarshal(raw, &o) err = json.Unmarshal(raw, &o)
assert.Nil(t, err) require.NoError(t, err)
return &o return &o
} }

View File

@ -12,7 +12,7 @@ import (
) )
// Describe describes a resource. // Describe describes a resource.
func Describe(c client.Connection, gvr client.GVR, path string) (string, error) { func Describe(c client.Connection, gvr *client.GVR, path string) (string, error) {
mapper := RestMapper{Connection: c} mapper := RestMapper{Connection: c}
m, err := mapper.ToRESTMapper() m, err := mapper.ToRESTMapper()
if err != nil { if err != nil {

View File

@ -27,14 +27,14 @@ type Dir struct {
// NewDir returns a new set of aliases. // NewDir returns a new set of aliases.
func NewDir(f Factory) *Dir { func NewDir(f Factory) *Dir {
var a Dir var a Dir
a.Init(f, client.NewGVR("dir")) a.Init(f, client.DirGVR)
return &a return &a
} }
var yamlRX = regexp.MustCompile(`.*\.(yml|yaml|json)`) var yamlRX = regexp.MustCompile(`.*\.(yml|yaml|json)`)
// List returns a collection of aliases. // List returns a collection of aliases.
func (a *Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) { func (*Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) {
dir, ok := ctx.Value(internal.KeyPath).(string) dir, ok := ctx.Value(internal.KeyPath).(string)
if !ok { if !ok {
return nil, errors.New("no dir in context") return nil, errors.New("no dir in context")
@ -60,6 +60,6 @@ func (a *Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) {
} }
// Get fetch a resource. // Get fetch a resource.
func (a *Dir) Get(_ context.Context, _ string) (runtime.Object, error) { func (*Dir) Get(_ context.Context, _ string) (runtime.Object, error) {
return nil, errors.New("nyi") return nil, errors.New("nyi")
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestNewDir(t *testing.T) { func TestNewDir(t *testing.T) {
@ -17,6 +18,6 @@ func TestNewDir(t *testing.T) {
ctx := context.WithValue(context.Background(), internal.KeyPath, "testdata/dir") ctx := context.WithValue(context.Background(), internal.KeyPath, "testdata/dir")
oo, err := d.List(ctx, "") oo, err := d.List(ctx, "")
assert.Nil(t, err) require.NoError(t, err)
assert.Equal(t, 2, len(oo)) assert.Len(t, oo, 2)
} }

View File

@ -41,7 +41,7 @@ type Deployment struct {
} }
// ListImages lists container images. // ListImages lists container images.
func (d *Deployment) ListImages(ctx context.Context, fqn string) ([]string, error) { func (d *Deployment) ListImages(_ context.Context, fqn string) ([]string, error) {
dp, err := d.GetInstance(fqn) dp, err := d.GetInstance(fqn)
if err != nil { if err != nil {
return nil, err return nil, err
@ -52,76 +52,12 @@ func (d *Deployment) ListImages(ctx context.Context, fqn string) ([]string, erro
// Scale a Deployment. // Scale a Deployment.
func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error { func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error {
ns, n := client.Namespaced(path) return scaleRes(ctx, d.getFactory(), client.DpGVR, path, replicas)
auth, err := d.Client().CanI(ns, "apps/v1/deployments:scale", n, []string{client.GetVerb, client.UpdateVerb})
if err != nil {
return err
}
if !auth {
return fmt.Errorf("user is not authorized to scale a deployment")
}
dial, err := d.Client().Dial()
if err != nil {
return err
}
scale, err := dial.AppsV1().Deployments(ns).GetScale(ctx, n, metav1.GetOptions{})
if err != nil {
return err
}
scale.Spec.Replicas = replicas
_, err = dial.AppsV1().Deployments(ns).UpdateScale(ctx, n, scale, metav1.UpdateOptions{})
return err
} }
// Restart a Deployment rollout. // Restart a Deployment rollout.
func (d *Deployment) Restart(ctx context.Context, path string) error { func (d *Deployment) Restart(ctx context.Context, path string) error {
o, err := d.getFactory().Get("apps/v1/deployments", path, true, labels.Everything()) return restartRes[*appsv1.Deployment](ctx, d.getFactory(), client.DpGVR, path)
if err != nil {
return err
}
var dp appsv1.Deployment
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp)
if err != nil {
return err
}
auth, err := d.Client().CanI(dp.Namespace, "apps/v1/deployments", dp.Name, client.PatchAccess)
if err != nil {
return err
}
if !auth {
return fmt.Errorf("user is not authorized to restart a deployment")
}
dial, err := d.Client().Dial()
if err != nil {
return err
}
before, err := runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), &dp)
if err != nil {
return err
}
after, err := polymorphichelpers.ObjectRestarterFn(&dp)
if err != nil {
return err
}
diff, err := strategicpatch.CreateTwoWayMergePatch(before, after, dp)
if err != nil {
return err
}
_, err = dial.AppsV1().Deployments(dp.Namespace).Patch(
ctx,
dp.Name,
types.StrategicMergePatchType,
diff,
metav1.PatchOptions{},
)
return err
} }
// TailLogs tail logs for all pods represented by this Deployment. // TailLogs tail logs for all pods represented by this Deployment.
@ -149,7 +85,7 @@ func (d *Deployment) Pod(fqn string) (string, error) {
// GetInstance fetch a matching deployment. // GetInstance fetch a matching deployment.
func (d *Deployment) GetInstance(fqn string) (*appsv1.Deployment, error) { func (d *Deployment) GetInstance(fqn string) (*appsv1.Deployment, error) {
o, err := d.Factory.Get(d.GVR(), fqn, true, labels.Everything()) o, err := d.Factory.Get(d.gvr, fqn, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -164,9 +100,9 @@ func (d *Deployment) GetInstance(fqn string) (*appsv1.Deployment, error) {
} }
// ScanSA scans for serviceaccount refs. // ScanSA scans for serviceaccount refs.
func (d *Deployment) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { func (d *Deployment) ScanSA(_ context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn) ns, n := client.Namespaced(fqn)
oo, err := d.getFactory().List(d.GVR(), ns, wait, labels.Everything()) oo, err := d.getFactory().List(d.gvr, ns, wait, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -190,9 +126,9 @@ func (d *Deployment) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, e
} }
// Scan scans for resource references. // Scan scans for resource references.
func (d *Deployment) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) { func (d *Deployment) Scan(_ context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn) ns, n := client.Namespaced(fqn)
oo, err := d.getFactory().List(d.GVR(), ns, wait, labels.Everything()) oo, err := d.getFactory().List(d.gvr, ns, wait, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -205,7 +141,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr client.GVR, fqn string, wait
return nil, errors.New("expecting Deployment resource") return nil, errors.New("expecting Deployment resource")
} }
switch gvr { switch gvr {
case CmGVR: case client.CmGVR:
if !hasConfigMap(&dp.Spec.Template.Spec, n) { if !hasConfigMap(&dp.Spec.Template.Spec, n) {
continue continue
} }
@ -213,7 +149,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr client.GVR, fqn string, wait
GVR: d.GVR(), GVR: d.GVR(),
FQN: client.FQN(dp.Namespace, dp.Name), FQN: client.FQN(dp.Namespace, dp.Name),
}) })
case SecGVR: case client.SecGVR:
found, err := hasSecret(d.Factory, &dp.Spec.Template.Spec, dp.Namespace, n, wait) found, err := hasSecret(d.Factory, &dp.Spec.Template.Spec, dp.Namespace, n, wait)
if err != nil { if err != nil {
slog.Warn("Fail to locate secret", slog.Warn("Fail to locate secret",
@ -229,7 +165,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr client.GVR, fqn string, wait
GVR: d.GVR(), GVR: d.GVR(),
FQN: client.FQN(dp.Namespace, dp.Name), FQN: client.FQN(dp.Namespace, dp.Name),
}) })
case PvcGVR: case client.PvcGVR:
if !hasPVC(&dp.Spec.Template.Spec, n) { if !hasPVC(&dp.Spec.Template.Spec, n) {
continue continue
} }
@ -237,7 +173,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr client.GVR, fqn string, wait
GVR: d.GVR(), GVR: d.GVR(),
FQN: client.FQN(dp.Namespace, dp.Name), FQN: client.FQN(dp.Namespace, dp.Name),
}) })
case PcGVR: case client.PcGVR:
if !hasPC(&dp.Spec.Template.Spec, n) { if !hasPC(&dp.Spec.Template.Spec, n) {
continue continue
} }
@ -246,7 +182,6 @@ func (d *Deployment) Scan(ctx context.Context, gvr client.GVR, fqn string, wait
FQN: client.FQN(dp.Namespace, dp.Name), FQN: client.FQN(dp.Namespace, dp.Name),
}) })
} }
} }
return refs, nil return refs, nil
@ -265,7 +200,7 @@ func (d *Deployment) GetPodSpec(path string) (*v1.PodSpec, error) {
// SetImages sets container images. // SetImages sets container images.
func (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error { func (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := d.Client().CanI(ns, "apps/v1/deployments", n, client.PatchAccess) auth, err := d.Client().CanI(ns, d.gvr, n, client.PatchAccess)
if err != nil { if err != nil {
return err return err
} }
@ -290,9 +225,11 @@ func (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs Imag
return err return err
} }
// Helpers...
func hasPVC(spec *v1.PodSpec, name string) bool { func hasPVC(spec *v1.PodSpec, name string) bool {
for _, v := range spec.Volumes { for i := range spec.Volumes {
if v.PersistentVolumeClaim != nil && v.PersistentVolumeClaim.ClaimName == name { if spec.Volumes[i].PersistentVolumeClaim != nil && spec.Volumes[i].PersistentVolumeClaim.ClaimName == name {
return true return true
} }
} }
@ -304,24 +241,24 @@ func hasPC(spec *v1.PodSpec, name string) bool {
} }
func hasConfigMap(spec *v1.PodSpec, name string) bool { func hasConfigMap(spec *v1.PodSpec, name string) bool {
for _, c := range spec.InitContainers { for i := range spec.InitContainers {
if containerHasConfigMap(c.EnvFrom, c.Env, name) { if containerHasConfigMap(spec.InitContainers[i].EnvFrom, spec.InitContainers[i].Env, name) {
return true return true
} }
} }
for _, c := range spec.Containers { for i := range spec.Containers {
if containerHasConfigMap(c.EnvFrom, c.Env, name) { if containerHasConfigMap(spec.Containers[i].EnvFrom, spec.Containers[i].Env, name) {
return true return true
} }
} }
for _, c := range spec.EphemeralContainers { for i := range spec.EphemeralContainers {
if containerHasConfigMap(c.EnvFrom, c.Env, name) { if containerHasConfigMap(spec.EphemeralContainers[i].EnvFrom, spec.EphemeralContainers[i].Env, name) {
return true return true
} }
} }
for _, v := range spec.Volumes { for i := range spec.Volumes {
if cm := v.ConfigMap; cm != nil { if cm := spec.Volumes[i].ConfigMap; cm != nil {
if cm.Name == name { if cm.Name == name {
return true return true
} }
@ -331,20 +268,20 @@ func hasConfigMap(spec *v1.PodSpec, name string) bool {
} }
func hasSecret(f Factory, spec *v1.PodSpec, ns, name string, wait bool) (bool, error) { func hasSecret(f Factory, spec *v1.PodSpec, ns, name string, wait bool) (bool, error) {
for _, c := range spec.InitContainers { for i := range spec.InitContainers {
if containerHasSecret(c.EnvFrom, c.Env, name) { if containerHasSecret(spec.InitContainers[i].EnvFrom, spec.InitContainers[i].Env, name) {
return true, nil return true, nil
} }
} }
for _, c := range spec.Containers { for i := range spec.Containers {
if containerHasSecret(c.EnvFrom, c.Env, name) { if containerHasSecret(spec.Containers[i].EnvFrom, spec.Containers[i].Env, name) {
return true, nil return true, nil
} }
} }
for _, c := range spec.EphemeralContainers { for i := range spec.EphemeralContainers {
if containerHasSecret(c.EnvFrom, c.Env, name) { if containerHasSecret(spec.EphemeralContainers[i].EnvFrom, spec.EphemeralContainers[i].Env, name) {
return true, nil return true, nil
} }
} }
@ -356,7 +293,7 @@ func hasSecret(f Factory, spec *v1.PodSpec, ns, name string, wait bool) (bool, e
} }
if saName := spec.ServiceAccountName; saName != "" { if saName := spec.ServiceAccountName; saName != "" {
o, err := f.Get("v1/serviceaccounts", client.FQN(ns, saName), wait, labels.Everything()) o, err := f.Get(client.SaGVR, client.FQN(ns, saName), wait, labels.Everything())
if err != nil { if err != nil {
return false, err return false, err
} }
@ -374,13 +311,14 @@ func hasSecret(f Factory, spec *v1.PodSpec, ns, name string, wait bool) (bool, e
} }
} }
for _, v := range spec.Volumes { for i := range spec.Volumes {
if sec := v.Secret; sec != nil { if sec := spec.Volumes[i].Secret; sec != nil {
if sec.SecretName == name { if sec.SecretName == name {
return true, nil return true, nil
} }
} }
} }
return false, nil return false, nil
} }
@ -419,3 +357,110 @@ func containerHasConfigMap(envFrom []v1.EnvFromSource, env []v1.EnvVar, name str
return false return false
} }
func scaleRes(ctx context.Context, f Factory, gvr *client.GVR, path string, replicas int32) error {
ns, n := client.Namespaced(path)
auth, err := f.Client().CanI(ns, client.NewGVR(gvr.String()+":scale"), n, []string{client.GetVerb, client.UpdateVerb})
if err != nil {
return err
}
if !auth {
return fmt.Errorf("user is not authorized to scale: %s", gvr)
}
dial, err := f.Client().Dial()
if err != nil {
return err
}
switch gvr {
case client.DpGVR:
scale, e := dial.AppsV1().Deployments(ns).GetScale(ctx, n, metav1.GetOptions{})
if e != nil {
return e
}
scale.Spec.Replicas = replicas
_, e = dial.AppsV1().Deployments(ns).UpdateScale(ctx, n, scale, metav1.UpdateOptions{})
return e
case client.StsGVR:
scale, e := dial.AppsV1().StatefulSets(ns).GetScale(ctx, n, metav1.GetOptions{})
if e != nil {
return e
}
scale.Spec.Replicas = replicas
_, e = dial.AppsV1().StatefulSets(ns).UpdateScale(ctx, n, scale, metav1.UpdateOptions{})
return e
default:
return fmt.Errorf("unsupported resource for scaling: %s", gvr)
}
}
func restartRes[T runtime.Object](ctx context.Context, f Factory, gvr *client.GVR, path string) error {
o, err := f.Get(gvr, path, true, labels.Everything())
if err != nil {
return err
}
var r = new(T)
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, r)
if err != nil {
return err
}
ns, n := client.Namespaced(path)
auth, err := f.Client().CanI(ns, gvr, n, client.PatchAccess)
if err != nil {
return err
}
if !auth {
return fmt.Errorf("user is not authorized to restart %q", gvr)
}
dial, err := f.Client().Dial()
if err != nil {
return err
}
before, err := runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), *r)
if err != nil {
return err
}
after, err := polymorphichelpers.ObjectRestarterFn(*r)
if err != nil {
return err
}
diff, err := strategicpatch.CreateTwoWayMergePatch(before, after, *r)
if err != nil {
return err
}
switch gvr {
case client.DpGVR:
_, err = dial.AppsV1().Deployments(ns).Patch(
ctx,
n,
types.StrategicMergePatchType,
diff,
metav1.PatchOptions{},
)
case client.DsGVR:
_, err = dial.AppsV1().DaemonSets(ns).Patch(
ctx,
n,
types.StrategicMergePatchType,
diff,
metav1.PatchOptions{},
)
case client.StsGVR:
_, err = dial.AppsV1().StatefulSets(ns).Patch(
ctx,
n,
types.StrategicMergePatchType,
diff,
metav1.PatchOptions{},
)
}
return err
}

View File

@ -22,9 +22,6 @@ import (
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/kubectl/pkg/polymorphichelpers"
"k8s.io/kubectl/pkg/scheme"
) )
var ( var (
@ -43,7 +40,7 @@ type DaemonSet struct {
} }
// ListImages lists container images. // ListImages lists container images.
func (d *DaemonSet) ListImages(ctx context.Context, fqn string) ([]string, error) { func (d *DaemonSet) ListImages(_ context.Context, fqn string) ([]string, error) {
ds, err := d.GetInstance(fqn) ds, err := d.GetInstance(fqn)
if err != nil { if err != nil {
return nil, err return nil, err
@ -54,51 +51,7 @@ func (d *DaemonSet) ListImages(ctx context.Context, fqn string) ([]string, error
// Restart a DaemonSet rollout. // Restart a DaemonSet rollout.
func (d *DaemonSet) Restart(ctx context.Context, path string) error { func (d *DaemonSet) Restart(ctx context.Context, path string) error {
o, err := d.getFactory().Get("apps/v1/daemonsets", path, true, labels.Everything()) return restartRes[*appsv1.DaemonSet](ctx, d.getFactory(), client.DsGVR, path)
if err != nil {
return err
}
var ds appsv1.DaemonSet
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
if err != nil {
return err
}
auth, err := d.Client().CanI(ds.Namespace, "apps/v1/daemonsets", ds.Name, client.PatchAccess)
if err != nil {
return err
}
if !auth {
return fmt.Errorf("user is not authorized to restart a daemonset")
}
dial, err := d.Client().Dial()
if err != nil {
return err
}
before, err := runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), &ds)
if err != nil {
return err
}
after, err := polymorphichelpers.ObjectRestarterFn(&ds)
if err != nil {
return err
}
diff, err := strategicpatch.CreateTwoWayMergePatch(before, after, ds)
if err != nil {
return err
}
_, err = dial.AppsV1().DaemonSets(ds.Namespace).Patch(
ctx,
ds.Name,
types.StrategicMergePatchType,
diff,
metav1.PatchOptions{},
)
return err
} }
// TailLogs tail logs for all pods represented by this DaemonSet. // TailLogs tail logs for all pods represented by this DaemonSet.
@ -130,14 +83,14 @@ func podLogs(ctx context.Context, sel map[string]string, opts *LogOptions) ([]Lo
} }
ns, _ := client.Namespaced(opts.Path) ns, _ := client.Namespaced(opts.Path)
oo, err := f.List("v1/pods", ns, true, lsel) oo, err := f.List(client.PodGVR, ns, true, lsel)
if err != nil { if err != nil {
return nil, err return nil, err
} }
opts.MultiPods = true opts.MultiPods = true
var po Pod var po Pod
po.Init(f, client.NewGVR("v1/pods")) po.Init(f, client.PodGVR)
outs := make([]LogChan, 0, len(oo)) outs := make([]LogChan, 0, len(oo))
for _, o := range oo { for _, o := range oo {
@ -169,7 +122,7 @@ func (d *DaemonSet) Pod(fqn string) (string, error) {
// GetInstance returns a daemonset instance. // GetInstance returns a daemonset instance.
func (d *DaemonSet) GetInstance(fqn string) (*appsv1.DaemonSet, error) { func (d *DaemonSet) GetInstance(fqn string) (*appsv1.DaemonSet, error) {
o, err := d.getFactory().Get(d.gvrStr(), fqn, true, labels.Everything()) o, err := d.getFactory().Get(d.gvr, fqn, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -184,9 +137,9 @@ func (d *DaemonSet) GetInstance(fqn string) (*appsv1.DaemonSet, error) {
} }
// ScanSA scans for serviceaccount refs. // ScanSA scans for serviceaccount refs.
func (d *DaemonSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { func (d *DaemonSet) ScanSA(_ context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn) ns, n := client.Namespaced(fqn)
oo, err := d.getFactory().List(d.GVR(), ns, wait, labels.Everything()) oo, err := d.getFactory().List(d.gvr, ns, wait, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -210,9 +163,9 @@ func (d *DaemonSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, er
} }
// Scan scans for cluster refs. // Scan scans for cluster refs.
func (d *DaemonSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) { func (d *DaemonSet) Scan(_ context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn) ns, n := client.Namespaced(fqn)
oo, err := d.getFactory().List(d.GVR(), ns, wait, labels.Everything()) oo, err := d.getFactory().List(d.gvr, ns, wait, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -225,7 +178,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait b
return nil, errors.New("expecting StatefulSet resource") return nil, errors.New("expecting StatefulSet resource")
} }
switch gvr { switch gvr {
case CmGVR: case client.CmGVR:
if !hasConfigMap(&ds.Spec.Template.Spec, n) { if !hasConfigMap(&ds.Spec.Template.Spec, n) {
continue continue
} }
@ -233,7 +186,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait b
GVR: d.GVR(), GVR: d.GVR(),
FQN: client.FQN(ds.Namespace, ds.Name), FQN: client.FQN(ds.Namespace, ds.Name),
}) })
case SecGVR: case client.SecGVR:
found, err := hasSecret(d.Factory, &ds.Spec.Template.Spec, ds.Namespace, n, wait) found, err := hasSecret(d.Factory, &ds.Spec.Template.Spec, ds.Namespace, n, wait)
if err != nil { if err != nil {
slog.Warn("Unable to locate secret", slog.Warn("Unable to locate secret",
@ -249,7 +202,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait b
GVR: d.GVR(), GVR: d.GVR(),
FQN: client.FQN(ds.Namespace, ds.Name), FQN: client.FQN(ds.Namespace, ds.Name),
}) })
case PvcGVR: case client.PvcGVR:
if !hasPVC(&ds.Spec.Template.Spec, n) { if !hasPVC(&ds.Spec.Template.Spec, n) {
continue continue
} }
@ -257,7 +210,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait b
GVR: d.GVR(), GVR: d.GVR(),
FQN: client.FQN(ds.Namespace, ds.Name), FQN: client.FQN(ds.Namespace, ds.Name),
}) })
case PcGVR: case client.PcGVR:
if !hasPC(&ds.Spec.Template.Spec, n) { if !hasPC(&ds.Spec.Template.Spec, n) {
continue continue
} }
@ -284,7 +237,7 @@ func (d *DaemonSet) GetPodSpec(path string) (*v1.PodSpec, error) {
// SetImages sets container images. // SetImages sets container images.
func (d *DaemonSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error { func (d *DaemonSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := d.Client().CanI(ns, "apps/v1/daemonset", n, client.PatchAccess) auth, err := d.Client().CanI(ns, d.gvr, n, client.PatchAccess)
if err != nil { if err != nil {
return err return err
} }

View File

@ -40,13 +40,11 @@ func (d *Dynamic) List(ctx context.Context, ns string) ([]runtime.Object, error)
func (d *Dynamic) toTable(ctx context.Context, fqn string) ([]runtime.Object, error) { func (d *Dynamic) toTable(ctx context.Context, fqn string) ([]runtime.Object, error) {
strLabel, _ := ctx.Value(internal.KeyLabels).(string) strLabel, _ := ctx.Value(internal.KeyLabels).(string)
opts := []string{d.gvr.AsResourceName()} opts := []string{d.gvr.AsResourceName()}
ns, n := client.Namespaced(fqn) ns, n := client.Namespaced(fqn)
if n != "" { if n != "" {
opts = append(opts, n) opts = append(opts, n)
} }
allNS := client.IsAllNamespaces(ns) allNS := client.IsAllNamespaces(ns)
flags := cmdutil.NewMatchVersionFlags(d.getFactory().Client().Config().Flags()) flags := cmdutil.NewMatchVersionFlags(d.getFactory().Client().Config().Flags())
f := cmdutil.NewFactory(flags) f := cmdutil.NewFactory(flags)
@ -70,7 +68,6 @@ func (d *Dynamic) toTable(ctx context.Context, fqn string) ([]runtime.Object, er
if err != nil { if err != nil {
return nil, err return nil, err
} }
oo := make([]runtime.Object, 0, len(infos)) oo := make([]runtime.Object, 0, len(infos))
for _, info := range infos { for _, info := range infos {
o, err := decodeIntoTable(info.Object, allNS) o, err := decodeIntoTable(info.Object, allNS)
@ -93,7 +90,6 @@ func decodeIntoTable(obj runtime.Object, allNs bool) (runtime.Object, error) {
if isEvent { if isEvent {
obj = event.Object.Object obj = event.Object.Object
} }
if !recognizedTableVersions[obj.GetObjectKind().GroupVersionKind()] { if !recognizedTableVersions[obj.GetObjectKind().GroupVersionKind()] {
return nil, fmt.Errorf("attempt to decode non-Table object: %v", obj.GetObjectKind().GroupVersionKind()) return nil, fmt.Errorf("attempt to decode non-Table object: %v", obj.GetObjectKind().GroupVersionKind())
} }
@ -133,7 +129,7 @@ func decodeIntoTable(obj runtime.Object, allNs bool) (runtime.Object, error) {
ns = m.GetNamespace() ns = m.GetNamespace()
} }
if allNs { if allNs {
cells := make([]interface{}, 0, len(row.Cells)+1) cells := make([]any, 0, len(row.Cells)+1)
cells = append(cells, ns) cells = append(cells, ns)
cells = append(cells, row.Cells...) cells = append(cells, row.Cells...)
row.Cells = cells row.Cells = cells

View File

@ -104,7 +104,7 @@ func (g *Generic) ToYAML(path string, showManaged bool) (string, error) {
// Delete deletes a resource. // Delete deletes a resource.
func (g *Generic) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error { func (g *Generic) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := g.Client().CanI(ns, g.gvrStr(), n, []string{client.DeleteVerb}) auth, err := g.Client().CanI(ns, g.gvr, n, []string{client.DeleteVerb})
if err != nil { if err != nil {
return err return err
} }

View File

@ -32,7 +32,7 @@ type HelmChart struct {
} }
// List returns a collection of resources. // List returns a collection of resources.
func (h *HelmChart) List(ctx context.Context, ns string) ([]runtime.Object, error) { func (h *HelmChart) List(_ context.Context, ns string) ([]runtime.Object, error) {
cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns) cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns)
if err != nil { if err != nil {
return nil, err return nil, err
@ -102,7 +102,7 @@ func (h *HelmChart) Describe(path string) (string, error) {
} }
// ToYAML returns the chart manifest. // ToYAML returns the chart manifest.
func (h *HelmChart) ToYAML(path string, showManaged bool) (string, error) { func (h *HelmChart) ToYAML(path string, _ bool) (string, error) {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns) cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns)
if err != nil { if err != nil {
@ -164,7 +164,7 @@ func ensureHelmConfig(flags *genericclioptions.ConfigFlags, ns string) (*action.
return cfg, err return cfg, err
} }
func helmLogger(fmat string, args ...interface{}) { func helmLogger(fmat string, args ...any) {
slog.Debug("Log", slog.Debug("Log",
slogs.Log, fmt.Sprintf(fmat, args...), slogs.Log, fmt.Sprintf(fmat, args...),
slogs.Subsys, "helm", slogs.Subsys, "helm",

View File

@ -59,7 +59,7 @@ func (h *HelmHistory) List(ctx context.Context, _ string) ([]runtime.Object, err
// Get returns a resource. // Get returns a resource.
func (h *HelmHistory) Get(_ context.Context, path string) (runtime.Object, error) { func (h *HelmHistory) Get(_ context.Context, path string) (runtime.Object, error) {
fqn, rev, found := strings.Cut(path, ":") fqn, rev, found := strings.Cut(path, ":")
if !found || len(rev) == 0 { if !found || rev == "" {
return nil, fmt.Errorf("invalid path %q", path) return nil, fmt.Errorf("invalid path %q", path)
} }
@ -99,7 +99,7 @@ func (h *HelmHistory) Describe(path string) (string, error) {
} }
// ToYAML returns the chart manifest. // ToYAML returns the chart manifest.
func (h *HelmHistory) ToYAML(path string, showManaged bool) (string, error) { func (h *HelmHistory) ToYAML(path string, _ bool) (string, error) {
rel, err := h.Get(context.Background(), path) rel, err := h.Get(context.Background(), path)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -27,14 +27,14 @@ const (
) )
// GetDefaultContainer returns a container name if specified in an annotation. // GetDefaultContainer returns a container name if specified in an annotation.
func GetDefaultContainer(m metav1.ObjectMeta, spec v1.PodSpec) (string, bool) { func GetDefaultContainer(m *metav1.ObjectMeta, spec *v1.PodSpec) (string, bool) {
defaultContainer, ok := m.Annotations[DefaultContainerAnnotation] defaultContainer, ok := m.Annotations[DefaultContainerAnnotation]
if !ok { if !ok {
return "", false return "", false
} }
for _, container := range spec.Containers { for i := range spec.Containers {
if container.Name == defaultContainer { if spec.Containers[i].Name == defaultContainer {
return defaultContainer, true return defaultContainer, true
} }
} }
@ -93,7 +93,7 @@ func ToYAML(o runtime.Object, showManaged bool) (string, error) {
if !showManaged { if !showManaged {
o = o.DeepCopyObject() o = o.DeepCopyObject()
uo := o.(*unstructured.Unstructured).Object uo := o.(*unstructured.Unstructured).Object
if meta, ok := uo["metadata"].(map[string]interface{}); ok { if meta, ok := uo["metadata"].(map[string]any); ok {
delete(meta, "managedFields") delete(meta, "managedFields")
} }
} }

View File

@ -20,6 +20,7 @@ func TestToPerc(t *testing.T) {
} }
for _, u := range uu { for _, u := range uu {
//nolint:testifylint
assert.Equal(t, u.e, toPerc(u.v1, u.v2)) assert.Equal(t, u.e, toPerc(u.v1, u.v2))
} }
} }

View File

@ -21,7 +21,7 @@ type ImageScan struct {
NonResource NonResource
} }
func (is *ImageScan) listImages(ctx context.Context, gvr client.GVR, path string) ([]string, error) { func (is *ImageScan) listImages(ctx context.Context, gvr *client.GVR, path string) ([]string, error) {
res, err := AccessorFor(is.Factory, gvr) res, err := AccessorFor(is.Factory, gvr)
if err != nil { if err != nil {
return nil, err return nil, err
@ -40,7 +40,7 @@ func (is *ImageScan) List(ctx context.Context, _ string) ([]runtime.Object, erro
if !ok { if !ok {
return nil, fmt.Errorf("no context path for %q", is.gvr) return nil, fmt.Errorf("no context path for %q", is.gvr)
} }
gvr, ok := ctx.Value(internal.KeyGVR).(client.GVR) gvr, ok := ctx.Value(internal.KeyGVR).(*client.GVR)
if !ok { if !ok {
return nil, fmt.Errorf("no context gvr for %q", is.gvr) return nil, fmt.Errorf("no context gvr for %q", is.gvr)
} }

View File

@ -32,7 +32,7 @@ type Job struct {
} }
// ListImages lists container images. // ListImages lists container images.
func (j *Job) ListImages(ctx context.Context, fqn string) ([]string, error) { func (j *Job) ListImages(_ context.Context, fqn string) ([]string, error) {
job, err := j.GetInstance(fqn) job, err := j.GetInstance(fqn)
if err != nil { if err != nil {
return nil, err return nil, err
@ -74,7 +74,7 @@ func (j *Job) List(ctx context.Context, ns string) ([]runtime.Object, error) {
// TailLogs tail logs for all pods represented by this Job. // TailLogs tail logs for all pods represented by this Job.
func (j *Job) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) { func (j *Job) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {
o, err := j.getFactory().Get(j.gvrStr(), opts.Path, true, labels.Everything()) o, err := j.getFactory().Get(j.gvr, opts.Path, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -93,7 +93,7 @@ func (j *Job) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error)
} }
func (j *Job) GetInstance(fqn string) (*batchv1.Job, error) { func (j *Job) GetInstance(fqn string) (*batchv1.Job, error) {
o, err := j.getFactory().Get(j.gvrStr(), fqn, true, labels.Everything()) o, err := j.getFactory().Get(j.gvr, fqn, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -108,9 +108,9 @@ func (j *Job) GetInstance(fqn string) (*batchv1.Job, error) {
} }
// ScanSA scans for serviceaccount refs. // ScanSA scans for serviceaccount refs.
func (j *Job) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { func (j *Job) ScanSA(_ context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn) ns, n := client.Namespaced(fqn)
oo, err := j.getFactory().List(j.GVR(), ns, wait, labels.Everything()) oo, err := j.getFactory().List(j.gvr, ns, wait, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -134,9 +134,9 @@ func (j *Job) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
} }
// Scan scans for resource references. // Scan scans for resource references.
func (j *Job) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) { func (j *Job) Scan(_ context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn) ns, n := client.Namespaced(fqn)
oo, err := j.getFactory().List(j.GVR(), ns, wait, labels.Everything()) oo, err := j.getFactory().List(j.gvr, ns, wait, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -149,7 +149,7 @@ func (j *Job) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (
return nil, errors.New("expecting Job resource") return nil, errors.New("expecting Job resource")
} }
switch gvr { switch gvr {
case CmGVR: case client.CmGVR:
if !hasConfigMap(&job.Spec.Template.Spec, n) { if !hasConfigMap(&job.Spec.Template.Spec, n) {
continue continue
} }
@ -157,7 +157,7 @@ func (j *Job) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (
GVR: j.GVR(), GVR: j.GVR(),
FQN: client.FQN(job.Namespace, job.Name), FQN: client.FQN(job.Namespace, job.Name),
}) })
case SecGVR: case client.SecGVR:
found, err := hasSecret(j.Factory, &job.Spec.Template.Spec, job.Namespace, n, wait) found, err := hasSecret(j.Factory, &job.Spec.Template.Spec, job.Namespace, n, wait)
if err != nil { if err != nil {
slog.Warn("Locate secret failed", slog.Warn("Locate secret failed",
@ -173,7 +173,7 @@ func (j *Job) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (
GVR: j.GVR(), GVR: j.GVR(),
FQN: client.FQN(job.Namespace, job.Name), FQN: client.FQN(job.Namespace, job.Name),
}) })
case PcGVR: case client.PcGVR:
if !hasPC(&job.Spec.Template.Spec, n) { if !hasPC(&job.Spec.Template.Spec, n) {
continue continue
} }

View File

@ -84,11 +84,11 @@ func (l *LogItem) Render(paint string, showTime bool, bb *bytes.Buffer) {
} }
if !l.SingleContainer && l.Container != "" { if !l.SingleContainer && l.Container != "" {
if len(l.Pod) > 0 { if l.Pod != "" {
bb.WriteString(" ") bb.WriteString(" ")
} }
bb.WriteString("[" + paint + "::b]" + l.Container + "[-::-] ") bb.WriteString("[" + paint + "::b]" + l.Container + "[-::-] ")
} else if len(l.Pod) > 0 { } else if l.Pod != "" {
bb.WriteString("[-::] ") bb.WriteString("[-::] ")
} }

View File

@ -112,7 +112,7 @@ func BenchmarkLogItemRenderTS(b *testing.B) {
b.ResetTimer() b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()
for n := 0; n < b.N; n++ { for range b.N {
bb := bytes.NewBuffer(make([]byte, 0, i.Size())) bb := bytes.NewBuffer(make([]byte, 0, i.Size()))
i.Render("yellow", true, bb) i.Render("yellow", true, bb)
} }
@ -125,7 +125,7 @@ func BenchmarkLogItemRenderNoTS(b *testing.B) {
b.ResetTimer() b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()
for n := 0; n < b.N; n++ { for range b.N {
bb := bytes.NewBuffer(make([]byte, 0, i.Size())) bb := bytes.NewBuffer(make([]byte, 0, i.Size()))
i.Render("yellow", false, bb) i.Render("yellow", false, bb)
} }

View File

@ -171,25 +171,25 @@ func (l *LogItems) DumpDebug(m string) {
} }
// Filter filters out log items based on given filter. // Filter filters out log items based on given filter.
func (l *LogItems) Filter(index int, q string, showTime bool) ([]int, [][]int, error) { func (l *LogItems) Filter(index int, q string, showTime bool) (matches []int, indices [][]int, err error) {
if q == "" { if q == "" {
return nil, nil, nil return
} }
if f, ok := internal.IsFuzzySelector(q); ok { if f, ok := internal.IsFuzzySelector(q); ok {
mm, ii := l.fuzzyFilter(index, f, showTime) matches, indices = l.fuzzyFilter(index, f, showTime)
return mm, ii, nil return
} }
matches, indices, err := l.filterLogs(index, q, showTime) matches, indices, err = l.filterLogs(index, q, showTime)
if err != nil { if err != nil {
return nil, nil, err return
} }
return matches, indices, nil return matches, indices, nil
} }
func (l *LogItems) fuzzyFilter(index int, q string, showTime bool) ([]int, [][]int) { func (l *LogItems) fuzzyFilter(index int, q string, showTime bool) (matches []int, indices [][]int) {
q = strings.TrimSpace(q) q = strings.TrimSpace(q)
matches, indices := make([]int, 0, len(l.items)), make([][]int, 0, 10) matches, indices = make([]int, 0, len(l.items)), make([][]int, 0, len(l.items))
mm := fuzzy.Find(q, l.StrLines(index, showTime)) mm := fuzzy.Find(q, l.StrLines(index, showTime))
for _, m := range mm { for _, m := range mm {
matches = append(matches, m.Index) matches = append(matches, m.Index)
@ -199,7 +199,7 @@ func (l *LogItems) fuzzyFilter(index int, q string, showTime bool) ([]int, [][]i
return matches, indices return matches, indices
} }
func (l *LogItems) filterLogs(index int, q string, showTime bool) ([]int, [][]int, error) { func (l *LogItems) filterLogs(index int, q string, showTime bool) (matches []int, indices [][]int, err error) {
var invert bool var invert bool
if internal.IsInverseSelector(q) { if internal.IsInverseSelector(q) {
invert = true invert = true
@ -209,7 +209,7 @@ func (l *LogItems) filterLogs(index int, q string, showTime bool) ([]int, [][]in
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
matches, indices := make([]int, 0, len(l.items)), make([][]int, 0, 10) matches, indices = make([]int, 0, len(l.items)), make([][]int, 0, len(l.items))
ll := make([][]byte, len(l.items[index:])) ll := make([][]byte, len(l.items[index:]))
l.Lines(index, showTime, ll) l.Lines(index, showTime, ll)
for i, line := range ll { for i, line := range ll {

View File

@ -31,9 +31,10 @@ type LogOptions struct {
// Info returns the option pod and container info. // Info returns the option pod and container info.
func (o *LogOptions) Info() string { func (o *LogOptions) Info() string {
if len(o.Container) != 0 { if o.Container != "" {
return fmt.Sprintf("%s (%s)", o.Path, o.Container) return fmt.Sprintf("%s (%s)", o.Path, o.Container)
} }
return o.Path return o.Path
} }
@ -131,7 +132,7 @@ func (o *LogOptions) ToLogItem(bytes []byte) *LogItem {
return item return item
} }
func (o *LogOptions) ToErrLogItem(err error) *LogItem { func (*LogOptions) ToErrLogItem(err error) *LogItem {
t := time.Now().UTC().Format(time.RFC3339Nano) t := time.Now().UTC().Format(time.RFC3339Nano)
item := NewLogItem([]byte(fmt.Sprintf("%s [orange::b]%s[::-]\n", t, err))) item := NewLogItem([]byte(fmt.Sprintf("%s [orange::b]%s[::-]\n", t, err)))
item.IsError = true item.IsError = true

View File

@ -102,8 +102,8 @@ func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error {
} }
if !cordoned { if !cordoned {
if err = n.ToggleCordon(path, true); err != nil { if e := n.ToggleCordon(path, true); e != nil {
return err return e
} }
} }
@ -168,7 +168,13 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
} }
shouldCountPods, _ := ctx.Value(internal.KeyPodCounting).(bool) shouldCountPods, _ := ctx.Value(internal.KeyPodCounting).(bool)
var pods []runtime.Object
if shouldCountPods {
pods, err = n.getFactory().List(client.PodGVR, client.BlankNamespace, false, labels.Everything())
if err != nil {
slog.Error("Unable to list pods", slogs.Error, err)
}
}
res := make([]runtime.Object, 0, len(oo)) res := make([]runtime.Object, 0, len(oo))
for _, o := range oo { for _, o := range oo {
u, ok := o.(*unstructured.Unstructured) u, ok := o.(*unstructured.Unstructured)
@ -180,7 +186,7 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
_, name := client.Namespaced(fqn) _, name := client.Namespaced(fqn)
podCount := -1 podCount := -1
if shouldCountPods { if shouldCountPods {
podCount, err = n.CountPods(name) podCount, err = n.CountPods(pods, name)
if err != nil { if err != nil {
slog.Error("Unable to get pods count", slog.Error("Unable to get pods count",
slogs.ResName, name, slogs.ResName, name,
@ -199,21 +205,16 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
} }
// CountPods counts the pods scheduled on a given node. // CountPods counts the pods scheduled on a given node.
func (n *Node) CountPods(nodeName string) (int, error) { func (*Node) CountPods(oo []runtime.Object, nodeName string) (int, error) {
var count int var count int
oo, err := n.getFactory().List("v1/pods", client.BlankNamespace, false, labels.Everything())
if err != nil {
return 0, err
}
for _, o := range oo { for _, o := range oo {
u, ok := o.(*unstructured.Unstructured) u, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return count, fmt.Errorf("expecting *unstructured.Unstructured but got `%T", o) return count, fmt.Errorf("expecting *Unstructured but got `%T", o)
} }
spec, ok := u.Object["spec"].(map[string]interface{}) spec, ok := u.Object["spec"].(map[string]any)
if !ok { if !ok {
return count, fmt.Errorf("expecting interface map but got `%T", o) return count, fmt.Errorf("expecting spec interface map but got `%T", o)
} }
if node, ok := spec["nodeName"]; ok && node == nodeName { if node, ok := spec["nodeName"]; ok && node == nodeName {
count++ count++
@ -225,7 +226,7 @@ func (n *Node) CountPods(nodeName string) (int, error) {
// GetPods returns all pods running on given node. // GetPods returns all pods running on given node.
func (n *Node) GetPods(nodeName string) ([]*v1.Pod, error) { func (n *Node) GetPods(nodeName string) ([]*v1.Pod, error) {
oo, err := n.getFactory().List("v1/pods", client.BlankNamespace, false, labels.Everything()) oo, err := n.getFactory().List(client.PodGVR, client.BlankNamespace, false, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -258,9 +259,9 @@ func (n *Node) ensureCordoned(path string) (bool, error) {
// Helpers... // Helpers...
// FetchNode retrieves a node. // FetchNode retrieves a node.
func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) { func FetchNode(_ context.Context, f Factory, path string) (*v1.Node, error) {
_, n := client.Namespaced(path) _, n := client.Namespaced(path)
auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", n, client.GetAccess) auth, err := f.Client().CanI(client.ClusterScope, client.NodeGVR, n, client.GetAccess)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -268,7 +269,7 @@ func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) {
return nil, fmt.Errorf("user is not authorized to list nodes") return nil, fmt.Errorf("user is not authorized to list nodes")
} }
o, err := f.Get("v1/nodes", client.FQN(client.ClusterScope, path), true, labels.Everything()) o, err := f.Get(client.NodeGVR, client.FQN(client.ClusterScope, path), true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -283,8 +284,8 @@ func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) {
} }
// FetchNodes retrieves all nodes. // FetchNodes retrieves all nodes.
func FetchNodes(ctx context.Context, f Factory, labelsSel string) (*v1.NodeList, error) { func FetchNodes(_ context.Context, f Factory, _ string) (*v1.NodeList, error) {
auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", "", client.ListAccess) auth, err := f.Client().CanI(client.ClusterScope, client.NodeGVR, "", client.ListAccess)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -292,7 +293,7 @@ func FetchNodes(ctx context.Context, f Factory, labelsSel string) (*v1.NodeList,
return nil, fmt.Errorf("user is not authorized to list nodes") return nil, fmt.Errorf("user is not authorized to list nodes")
} }
oo, err := f.List("v1/nodes", "", false, labels.Everything()) oo, err := f.List(client.NodeGVR, "", false, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -16,20 +16,19 @@ import (
type NonResource struct { type NonResource struct {
Factory Factory
gvr client.GVR gvr *client.GVR
mx sync.RWMutex mx sync.RWMutex
includeObj bool includeObj bool
} }
// Init initializes the resource. // Init initializes the resource.
func (n *NonResource) Init(f Factory, gvr client.GVR) { func (n *NonResource) Init(f Factory, gvr *client.GVR) {
n.mx.Lock() n.mx.Lock()
{ n.Factory, n.gvr = f, gvr
n.Factory, n.gvr = f, gvr
}
n.mx.Unlock() n.mx.Unlock()
} }
// SetIncludeObject sets if resource object should be included in the api server response.
func (n *NonResource) SetIncludeObject(f bool) { func (n *NonResource) SetIncludeObject(f bool) {
n.includeObj = f n.includeObj = f
} }
@ -57,6 +56,6 @@ func (n *NonResource) GVR() string {
} }
// Get returns the given resource. // Get returns the given resource.
func (n *NonResource) Get(context.Context, string) (runtime.Object, error) { func (*NonResource) Get(context.Context, string) (runtime.Object, error) {
return nil, fmt.Errorf("nyi") return nil, fmt.Errorf("nyi")
} }

View File

@ -75,7 +75,7 @@ func getPatchPodSpec(imageSpecs ImageSpecs) PodSpec {
return podSpec return podSpec
} }
func extractElements(imageSpecs ImageSpecs) (initElementsOrders []Element, initElements []Element, elementsOrders []Element, elements []Element) { func extractElements(imageSpecs ImageSpecs) (initElementsOrders, initElements, elementsOrders, elements []Element) {
for _, spec := range imageSpecs { for _, spec := range imageSpecs {
if spec.Init { if spec.Init {
initElementsOrders = append(initElementsOrders, Element{Name: spec.Name}) initElementsOrders = append(initElementsOrders, Element{Name: spec.Name})

View File

@ -69,7 +69,7 @@ func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
} }
// ListImages lists container images. // ListImages lists container images.
func (p *Pod) ListImages(ctx context.Context, path string) ([]string, error) { func (p *Pod) ListImages(_ context.Context, path string) ([]string, error) {
pod, err := p.GetInstance(path) pod, err := p.GetInstance(path)
if err != nil { if err != nil {
return nil, err return nil, err
@ -108,7 +108,7 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
continue continue
} }
spec, ok := u.Object["spec"].(map[string]interface{}) spec, ok := u.Object["spec"].(map[string]any)
if !ok { if !ok {
return res, fmt.Errorf("expecting interface map but got `%T", o) return res, fmt.Errorf("expecting interface map but got `%T", o)
} }
@ -123,7 +123,7 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
// Logs fetch container logs for a given pod and container. // Logs fetch container logs for a given pod and container.
func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) { func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := p.Client().CanI(ns, "v1/pods:log", n, client.GetAccess) auth, err := p.Client().CanI(ns, client.NewGVR(client.PodGVR.String()+":log"), n, client.GetAccess)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -147,13 +147,13 @@ func (p *Pod) Containers(path string, includeInit bool) ([]string, error) {
} }
cc := make([]string, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers)) cc := make([]string, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers))
for _, c := range pod.Spec.Containers { for i := range pod.Spec.Containers {
cc = append(cc, c.Name) cc = append(cc, pod.Spec.Containers[i].Name)
} }
if includeInit { if includeInit {
for _, c := range pod.Spec.InitContainers { for i := range pod.Spec.InitContainers {
cc = append(cc, c.Name) cc = append(cc, pod.Spec.InitContainers[i].Name)
} }
} }
@ -161,13 +161,13 @@ func (p *Pod) Containers(path string, includeInit bool) ([]string, error) {
} }
// Pod returns a pod victim by name. // Pod returns a pod victim by name.
func (p *Pod) Pod(fqn string) (string, error) { func (*Pod) Pod(fqn string) (string, error) {
return fqn, nil return fqn, nil
} }
// GetInstance returns a pod instance. // GetInstance returns a pod instance.
func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) { func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) {
o, err := p.getFactory().Get(p.gvrStr(), fqn, true, labels.Everything()) o, err := p.getFactory().Get(p.gvr, fqn, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -187,7 +187,7 @@ func (p *Pod) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error)
if !ok { if !ok {
return nil, errors.New("no factory in context") return nil, errors.New("no factory in context")
} }
o, err := fac.Get(p.gvrStr(), opts.Path, true, labels.Everything()) o, err := fac.Get(p.gvr, opts.Path, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -201,26 +201,26 @@ func (p *Pod) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error)
} }
outs := make([]LogChan, 0, coCounts) outs := make([]LogChan, 0, coCounts)
if co, ok := GetDefaultContainer(po.ObjectMeta, po.Spec); ok && !opts.AllContainers { if co, ok := GetDefaultContainer(&po.ObjectMeta, &po.Spec); ok && !opts.AllContainers {
opts.DefaultContainer = co opts.DefaultContainer = co
return append(outs, tailLogs(ctx, p, opts)), nil return append(outs, tailLogs(ctx, p, opts)), nil
} }
if opts.HasContainer() && !opts.AllContainers { if opts.HasContainer() && !opts.AllContainers {
return append(outs, tailLogs(ctx, p, opts)), nil return append(outs, tailLogs(ctx, p, opts)), nil
} }
for _, co := range po.Spec.InitContainers { for i := range po.Spec.InitContainers {
cfg := opts.Clone() cfg := opts.Clone()
cfg.Container = co.Name cfg.Container = po.Spec.InitContainers[i].Name
outs = append(outs, tailLogs(ctx, p, cfg)) outs = append(outs, tailLogs(ctx, p, cfg))
} }
for _, co := range po.Spec.Containers { for i := range po.Spec.Containers {
cfg := opts.Clone() cfg := opts.Clone()
cfg.Container = co.Name cfg.Container = po.Spec.Containers[i].Name
outs = append(outs, tailLogs(ctx, p, cfg)) outs = append(outs, tailLogs(ctx, p, cfg))
} }
for _, co := range po.Spec.EphemeralContainers { for i := range po.Spec.EphemeralContainers {
cfg := opts.Clone() cfg := opts.Clone()
cfg.Container = co.Name cfg.Container = po.Spec.EphemeralContainers[i].Name
outs = append(outs, tailLogs(ctx, p, cfg)) outs = append(outs, tailLogs(ctx, p, cfg))
} }
@ -228,9 +228,9 @@ func (p *Pod) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error)
} }
// ScanSA scans for ServiceAccount refs. // ScanSA scans for ServiceAccount refs.
func (p *Pod) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { func (p *Pod) ScanSA(_ context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn) ns, n := client.Namespaced(fqn)
oo, err := p.getFactory().List(p.GVR(), ns, wait, labels.Everything()) oo, err := p.getFactory().List(p.gvr, ns, wait, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -258,9 +258,9 @@ func (p *Pod) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
} }
// Scan scans for cluster resource refs. // Scan scans for cluster resource refs.
func (p *Pod) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) { func (p *Pod) Scan(_ context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn) ns, n := client.Namespaced(fqn)
oo, err := p.getFactory().List(p.GVR(), ns, wait, labels.Everything()) oo, err := p.getFactory().List(p.gvr, ns, wait, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -277,7 +277,7 @@ func (p *Pod) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (
continue continue
} }
switch gvr { switch gvr {
case CmGVR: case client.CmGVR:
if !hasConfigMap(&pod.Spec, n) { if !hasConfigMap(&pod.Spec, n) {
continue continue
} }
@ -285,7 +285,7 @@ func (p *Pod) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (
GVR: p.GVR(), GVR: p.GVR(),
FQN: client.FQN(pod.Namespace, pod.Name), FQN: client.FQN(pod.Namespace, pod.Name),
}) })
case SecGVR: case client.SecGVR:
found, err := hasSecret(p.Factory, &pod.Spec, pod.Namespace, n, wait) found, err := hasSecret(p.Factory, &pod.Spec, pod.Namespace, n, wait)
if err != nil { if err != nil {
slog.Warn("Locate secret failed", slog.Warn("Locate secret failed",
@ -301,7 +301,7 @@ func (p *Pod) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (
GVR: p.GVR(), GVR: p.GVR(),
FQN: client.FQN(pod.Namespace, pod.Name), FQN: client.FQN(pod.Namespace, pod.Name),
}) })
case PvcGVR: case client.PvcGVR:
if !hasPVC(&pod.Spec, n) { if !hasPVC(&pod.Spec, n) {
continue continue
} }
@ -309,7 +309,7 @@ func (p *Pod) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (
GVR: p.GVR(), GVR: p.GVR(),
FQN: client.FQN(pod.Namespace, pod.Name), FQN: client.FQN(pod.Namespace, pod.Name),
}) })
case PcGVR: case client.PcGVR:
if !hasPC(&pod.Spec, n) { if !hasPC(&pod.Spec, n) {
continue continue
} }
@ -336,20 +336,20 @@ func tailLogs(ctx context.Context, logger Logger, opts *LogOptions) LogChan {
go func() { go func() {
defer wg.Done() defer wg.Done()
podOpts := opts.ToPodLogOptions() podOpts := opts.ToPodLogOptions()
for r := 0; r < logRetryCount; r++ { for range logRetryCount {
req, err := logger.Logs(opts.Path, podOpts) req, err := logger.Logs(opts.Path, podOpts)
if err == nil { if err == nil {
// This call will block if nothing is in the stream!! // This call will block if nothing is in the stream!!
if stream, e := req.Stream(ctx); e == nil { stream, e := req.Stream(ctx)
if e == nil {
wg.Add(1) wg.Add(1)
go readLogs(ctx, &wg, stream, out, opts) go readLogs(ctx, &wg, stream, out, opts)
return return
} else {
slog.Error("Stream logs failed",
slogs.Error, e,
slogs.Container, opts.Info(),
)
} }
slog.Error("Stream logs failed",
slogs.Error, e,
slogs.Container, opts.Info(),
)
} else { } else {
slog.Error("Log request failed", slog.Error("Log request failed",
slogs.Container, opts.Info(), slogs.Container, opts.Info(),
@ -419,7 +419,7 @@ func readLogs(ctx context.Context, wg *sync.WaitGroup, stream io.ReadCloser, out
} }
// 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 {
if m.Namespace == "" { if m.Namespace == "" {
return m.Name return m.Name
} }
@ -434,13 +434,14 @@ func (p *Pod) GetPodSpec(path string) (*v1.PodSpec, error) {
return nil, err return nil, err
} }
podSpec := pod.Spec podSpec := pod.Spec
return &podSpec, nil return &podSpec, nil
} }
// SetImages sets container images. // SetImages sets container images.
func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error { func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := p.Client().CanI(ns, "v1/pod", n, client.PatchAccess) auth, err := p.Client().CanI(ns, p.gvr, n, client.PatchAccess)
if err != nil { if err != nil {
return err return err
} }
@ -469,10 +470,11 @@ func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs)
jsonPatch, jsonPatch,
metav1.PatchOptions{}, metav1.PatchOptions{},
) )
return err return err
} }
func (p *Pod) isControlled(path string) (string, bool, error) { func (p *Pod) isControlled(path string) (fqn string, ok bool, err error) {
pod, err := p.GetInstance(path) pod, err := p.GetInstance(path)
if err != nil { if err != nil {
return "", false, err return "", false, err
@ -481,6 +483,7 @@ func (p *Pod) isControlled(path string) (string, bool, error) {
if len(references) > 0 { if len(references) > 0 {
return fmt.Sprintf("%s/%s", references[0].Kind, references[0].Name), true, nil return fmt.Sprintf("%s/%s", references[0].Kind, references[0].Name), true, nil
} }
return "", false, nil return "", false, nil
} }

View File

@ -51,7 +51,7 @@ func TestGetDefaultContainer(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) {
container, ok := GetDefaultContainer(u.po.ObjectMeta, u.po.Spec) container, ok := GetDefaultContainer(&u.po.ObjectMeta, &u.po.Spec)
assert.Equal(t, u.wantContainer, container) assert.Equal(t, u.wantContainer, container)
assert.Equal(t, u.wantOk, ok) assert.Equal(t, u.wantOk, ok)
}) })

View File

@ -122,7 +122,7 @@ func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.Por
p.path, p.tunnel, p.age = path, tt, time.Now() p.path, p.tunnel, p.age = path, tt, time.Now()
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := p.Client().CanI(ns, "v1/pods", n, client.GetAccess) auth, err := p.Client().CanI(ns, client.PodGVR, n, client.GetAccess)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -132,7 +132,7 @@ func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.Por
podName := strings.Split(n, "|")[0] podName := strings.Split(n, "|")[0]
var res Pod var res Pod
res.Init(p, client.NewGVR("v1/pods")) res.Init(p, client.PodGVR)
pod, err := res.GetInstance(client.FQN(ns, podName)) pod, err := res.GetInstance(client.FQN(ns, podName))
if err != nil { if err != nil {
return nil, err return nil, err
@ -141,7 +141,7 @@ func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.Por
return nil, fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase) return nil, fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase)
} }
auth, err = p.Client().CanI(ns, "v1/pods:portforward", "", []string{client.CreateVerb}) auth, err = p.Client().CanI(ns, client.PodGVR.WithSubResource("portforward"), "", []string{client.CreateVerb})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -16,6 +16,6 @@ type Pulse struct {
} }
// List lists out pulses. // List lists out pulses.
func (h *Pulse) List(ctx context.Context, ns string) ([]runtime.Object, error) { func (*Pulse) List(context.Context, string) ([]runtime.Object, error) {
return nil, fmt.Errorf("NYI") return nil, fmt.Errorf("NYI")
} }

View File

@ -18,13 +18,6 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
const (
crbGVR = "rbac.authorization.k8s.io/v1/clusterrolebindings"
crGVR = "rbac.authorization.k8s.io/v1/clusterroles"
rbGVR = "rbac.authorization.k8s.io/v1/rolebindings"
rGVR = "rbac.authorization.k8s.io/v1/roles"
)
var ( var (
_ Accessor = (*Rbac)(nil) _ Accessor = (*Rbac)(nil)
_ Nuker = (*Rbac)(nil) _ Nuker = (*Rbac)(nil)
@ -37,7 +30,7 @@ type Rbac struct {
// List lists out rbac resources. // List lists out rbac resources.
func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) { func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) {
gvr, ok := ctx.Value(internal.KeyGVR).(client.GVR) gvr, ok := ctx.Value(internal.KeyGVR).(*client.GVR)
if !ok { if !ok {
return nil, fmt.Errorf("expecting a context gvr") return nil, fmt.Errorf("expecting a context gvr")
} }
@ -61,23 +54,22 @@ func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) {
} }
func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) { func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) {
o, err := r.getFactory().Get(crbGVR, path, true, labels.Everything()) crbo, err := r.getFactory().Get(client.CrbGVR, path, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
var crb rbacv1.ClusterRoleBinding var crb rbacv1.ClusterRoleBinding
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb) err = runtime.DefaultUnstructuredConverter.FromUnstructured(crbo.(*unstructured.Unstructured).Object, &crb)
if err != nil { if err != nil {
return nil, err return nil, err
} }
crbo, err := r.getFactory().Get(crGVR, client.FQN("-", crb.RoleRef.Name), true, labels.Everything()) cro, err := r.getFactory().Get(client.CrGVR, client.FQN("-", crb.RoleRef.Name), true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
var cr rbacv1.ClusterRole var cr rbacv1.ClusterRole
err = runtime.DefaultUnstructuredConverter.FromUnstructured(crbo.(*unstructured.Unstructured).Object, &cr) err = runtime.DefaultUnstructuredConverter.FromUnstructured(crbo.(*unstructured.Unstructured).Object, &cro)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -86,30 +78,29 @@ func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) {
} }
func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) { func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) {
o, err := r.getFactory().Get(rbGVR, path, true, labels.Everything()) rbo, err := r.getFactory().Get(client.RobGVR, path, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
var rb rbacv1.RoleBinding var rb rbacv1.RoleBinding
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb); err != nil { if e := runtime.DefaultUnstructuredConverter.FromUnstructured(rbo.(*unstructured.Unstructured).Object, &rb); e != nil {
return nil, err return nil, e
} }
if rb.RoleRef.Kind == "ClusterRole" { if rb.RoleRef.Kind == "ClusterRole" {
o, e := r.getFactory().Get(crGVR, client.FQN("-", rb.RoleRef.Name), true, labels.Everything()) cro, e := r.getFactory().Get(client.CrGVR, client.FQN("-", rb.RoleRef.Name), true, labels.Everything())
if e != nil { if e != nil {
return nil, e return nil, e
} }
var cr rbacv1.ClusterRole var cr rbacv1.ClusterRole
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr) err = runtime.DefaultUnstructuredConverter.FromUnstructured(cro.(*unstructured.Unstructured).Object, &cr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return asRuntimeObjects(parseRules(client.ClusterScope, "-", cr.Rules)), nil return asRuntimeObjects(parseRules(client.ClusterScope, "-", cr.Rules)), nil
} }
ro, err := r.getFactory().Get(rGVR, client.FQN(rb.Namespace, rb.RoleRef.Name), true, labels.Everything()) ro, err := r.getFactory().Get(client.RoGVR, client.FQN(rb.Namespace, rb.RoleRef.Name), true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -124,11 +115,10 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) {
func (r *Rbac) loadClusterRole(fqn string) ([]runtime.Object, error) { func (r *Rbac) loadClusterRole(fqn string) ([]runtime.Object, error) {
slog.Debug("LOAD-CR", slogs.FQN, fqn) slog.Debug("LOAD-CR", slogs.FQN, fqn)
o, err := r.getFactory().Get(crGVR, fqn, true, labels.Everything()) o, err := r.getFactory().Get(client.CrGVR, fqn, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
var cr rbacv1.ClusterRole var cr rbacv1.ClusterRole
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr) err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr)
if err != nil { if err != nil {
@ -139,11 +129,10 @@ func (r *Rbac) loadClusterRole(fqn string) ([]runtime.Object, error) {
} }
func (r *Rbac) loadRole(path string) ([]runtime.Object, error) { func (r *Rbac) loadRole(path string) ([]runtime.Object, error) {
o, err := r.getFactory().Get(rGVR, path, true, labels.Everything()) o, err := r.getFactory().Get(client.RoGVR, path, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
var ro rbacv1.Role var ro rbacv1.Role
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ro) err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ro)
if err != nil { if err != nil {

View File

@ -29,7 +29,7 @@ type Policy struct {
} }
// List returns available policies. // List returns available policies.
func (p *Policy) List(ctx context.Context, ns string) ([]runtime.Object, error) { func (p *Policy) List(ctx context.Context, _ string) ([]runtime.Object, error) {
kind, ok := ctx.Value(internal.KeySubjectKind).(string) kind, ok := ctx.Value(internal.KeySubjectKind).(string)
if !ok { if !ok {
return nil, fmt.Errorf("expecting a context subject kind") return nil, fmt.Errorf("expecting a context subject kind")
@ -67,11 +67,10 @@ func (p *Policy) loadClusterRoleBinding(kind, name string) (render.Policies, err
ns, n := client.Namespaced(name) ns, n := client.Namespaced(name)
var nn []string var nn []string
for _, crb := range crbs { for i := range crbs {
for _, s := range crb.Subjects { for _, s := range crbs[i].Subjects {
s := s
if isSameSubject(kind, ns, n, &s) { if isSameSubject(kind, ns, n, &s) {
nn = append(nn, crb.RoleRef.Name) nn = append(nn, crbs[i].RoleRef.Name)
} }
} }
} }
@ -81,11 +80,11 @@ func (p *Policy) loadClusterRoleBinding(kind, name string) (render.Policies, err
} }
rows := make(render.Policies, 0, len(nn)) rows := make(render.Policies, 0, len(nn))
for _, cr := range crs { for i := range crs {
if !inList(nn, cr.Name) { if !inList(nn, crs[i].Name) {
continue continue
} }
rows = append(rows, parseRules(client.NotNamespaced, "CR:"+cr.Name, cr.Rules)...) rows = append(rows, parseRules(client.NotNamespaced, "CR:"+crs[i].Name, crs[i].Rules)...)
} }
return rows, nil return rows, nil
@ -101,13 +100,13 @@ func (p *Policy) loadRoleBinding(kind, name string) (render.Policies, error) {
return nil, err return nil, err
} }
rows := make(render.Policies, 0, len(crs)) rows := make(render.Policies, 0, len(crs))
for _, cr := range crs { for i := range crs {
if rbNs, ok := rbsMap["ClusterRole:"+cr.Name]; ok { if rbNs, ok := rbsMap["ClusterRole:"+crs[i].Name]; ok {
slog.Debug("Loading rules for clusterrole", slog.Debug("Loading rules for clusterrole",
slogs.Namespace, rbNs, slogs.Namespace, rbNs,
slogs.ResName, cr.Name, slogs.ResName, crs[i].Name,
) )
rows = append(rows, parseRules(rbNs, "CR:"+cr.Name, cr.Rules)...) rows = append(rows, parseRules(rbNs, "CR:"+crs[i].Name, crs[i].Rules)...)
} }
} }
@ -115,22 +114,22 @@ func (p *Policy) loadRoleBinding(kind, name string) (render.Policies, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, ro := range ros { for i := range ros {
if _, ok := rbsMap["Role:"+ro.Name]; !ok { if _, ok := rbsMap["Role:"+ros[i].Name]; !ok {
continue continue
} }
slog.Debug("Loading rules for role", slog.Debug("Loading rules for role",
slogs.Namespace, ro.Namespace, slogs.Namespace, ros[i].Namespace,
slogs.ResName, ro.Name, slogs.ResName, ros[i].Name,
) )
rows = append(rows, parseRules(ro.Namespace, "RO:"+ro.Name, ro.Rules)...) rows = append(rows, parseRules(ros[i].Namespace, "RO:"+ros[i].Name, ros[i].Rules)...)
} }
return rows, nil return rows, nil
} }
func fetchClusterRoleBindings(f Factory) ([]rbacv1.ClusterRoleBinding, error) { func fetchClusterRoleBindings(f Factory) ([]rbacv1.ClusterRoleBinding, error) {
oo, err := f.List(crbGVR, client.ClusterScope, false, labels.Everything()) oo, err := f.List(client.CrbGVR, client.ClusterScope, false, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -148,7 +147,7 @@ func fetchClusterRoleBindings(f Factory) ([]rbacv1.ClusterRoleBinding, error) {
} }
func fetchRoleBindings(f Factory) ([]rbacv1.RoleBinding, error) { func fetchRoleBindings(f Factory) ([]rbacv1.RoleBinding, error) {
oo, err := f.List(rbGVR, client.ClusterScope, false, labels.Everything()) oo, err := f.List(client.RobGVR, client.ClusterScope, false, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -173,11 +172,10 @@ func (p *Policy) fetchRoleBindingNamespaces(kind, name string) (map[string]strin
ns, n := client.Namespaced(name) ns, n := client.Namespaced(name)
ss := make(map[string]string, len(rbs)) ss := make(map[string]string, len(rbs))
for _, rb := range rbs { for i := range rbs {
for _, s := range rb.Subjects { for _, s := range rbs[i].Subjects {
s := s
if isSameSubject(kind, ns, n, &s) { if isSameSubject(kind, ns, n, &s) {
ss[rb.RoleRef.Kind+":"+rb.RoleRef.Name] = rb.Namespace ss[rbs[i].RoleRef.Kind+":"+rbs[i].RoleRef.Name] = rbs[i].Namespace
} }
} }
} }
@ -200,9 +198,7 @@ func isSameSubject(kind, ns, name string, subject *rbacv1.Subject) bool {
} }
func (p *Policy) fetchClusterRoles() ([]rbacv1.ClusterRole, error) { func (p *Policy) fetchClusterRoles() ([]rbacv1.ClusterRole, error) {
const gvr = "rbac.authorization.k8s.io/v1/clusterroles" oo, err := p.getFactory().List(client.CrGVR, client.ClusterScope, false, labels.Everything())
oo, err := p.getFactory().List(gvr, client.ClusterScope, false, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -220,9 +216,7 @@ func (p *Policy) fetchClusterRoles() ([]rbacv1.ClusterRole, error) {
} }
func (p *Policy) fetchRoles() ([]rbacv1.Role, error) { func (p *Policy) fetchRoles() ([]rbacv1.Role, error) {
const gvr = "rbac.authorization.k8s.io/v1/roles" oo, err := p.getFactory().List(client.RoGVR, client.BlankNamespace, false, labels.Everything())
oo, err := p.getFactory().List(gvr, client.BlankNamespace, false, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -23,7 +23,7 @@ type Subject struct {
} }
// List returns a collection of subjects. // List returns a collection of subjects.
func (s *Subject) List(ctx context.Context, ns string) ([]runtime.Object, error) { func (s *Subject) List(ctx context.Context, _ string) ([]runtime.Object, error) {
kind, ok := ctx.Value(internal.KeySubjectKind).(string) kind, ok := ctx.Value(internal.KeySubjectKind).(string)
if !ok { if !ok {
return nil, errors.New("expecting a SubjectKind") return nil, errors.New("expecting a SubjectKind")
@ -57,15 +57,15 @@ func (s *Subject) listClusterRoleBindings(kind string) (render.Subjects, error)
} }
oo := make(render.Subjects, 0, len(crbs)) oo := make(render.Subjects, 0, len(crbs))
for _, crb := range crbs { for i := range crbs {
for _, su := range crb.Subjects { for _, su := range crbs[i].Subjects {
if su.Kind != kind { if su.Kind != kind {
continue continue
} }
oo = oo.Upsert(render.SubjectRes{ oo = oo.Upsert(render.SubjectRes{
Name: su.Name, Name: su.Name,
Kind: "ClusterRoleBinding", Kind: "ClusterRoleBinding",
FirstLocation: crb.Name, FirstLocation: crbs[i].Name,
}) })
} }
} }
@ -80,15 +80,15 @@ func (s *Subject) listRoleBindings(kind string) (render.Subjects, error) {
} }
oo := make(render.Subjects, 0, len(rbs)) oo := make(render.Subjects, 0, len(rbs))
for _, rb := range rbs { for i := range rbs {
for _, su := range rb.Subjects { for _, su := range rbs[i].Subjects {
if su.Kind != kind { if su.Kind != kind {
continue continue
} }
oo = oo.Upsert(render.SubjectRes{ oo = oo.Upsert(render.SubjectRes{
Name: su.Name, Name: su.Name,
Kind: "RoleBinding", Kind: "RoleBinding",
FirstLocation: rb.Name, FirstLocation: rbs[i].Name,
}) })
} }
} }

View File

@ -21,13 +21,13 @@ type Reference struct {
} }
// List collects all references. // List collects all references.
func (r *Reference) List(ctx context.Context, ns string) ([]runtime.Object, error) { func (r *Reference) List(ctx context.Context, _ string) ([]runtime.Object, error) {
gvr, ok := ctx.Value(internal.KeyGVR).(client.GVR) gvr, ok := ctx.Value(internal.KeyGVR).(*client.GVR)
if !ok { if !ok {
return nil, errors.New("no context for gvr found") return nil, errors.New("no context for gvr found")
} }
switch gvr { switch gvr {
case SaGVR: case client.SaGVR:
return r.ScanSA(ctx) return r.ScanSA(ctx)
default: default:
return r.Scan(ctx) return r.Scan(ctx)
@ -35,7 +35,7 @@ func (r *Reference) List(ctx context.Context, ns string) ([]runtime.Object, erro
} }
// Get fetch a given reference. // Get fetch a given reference.
func (r *Reference) Get(ctx context.Context, path string) (runtime.Object, error) { func (*Reference) Get(context.Context, string) (runtime.Object, error) {
panic("NYI") panic("NYI")
} }

View File

@ -6,8 +6,8 @@ package dao
import ( import (
"fmt" "fmt"
"log/slog" "log/slog"
"maps"
"slices" "slices"
"sort"
"strings" "strings"
"sync" "sync"
@ -27,12 +27,8 @@ const (
k9sCat = "k9s" k9sCat = "k9s"
helmCat = "helm" helmCat = "helm"
scaleCat = "scale" scaleCat = "scale"
crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
) )
// MetaAccess tracks resources metadata.
var MetaAccess = NewMeta()
var stdGroups = sets.New[string]( var stdGroups = sets.New[string](
"apps/v1", "apps/v1",
"autoscaling/v1", "autoscaling/v1",
@ -47,12 +43,18 @@ var stdGroups = sets.New[string](
"v1", "v1",
) )
// ResourceMetas represents a collection of resource metadata.
type ResourceMetas map[*client.GVR]*metav1.APIResource
func (m ResourceMetas) clear() { func (m ResourceMetas) clear() {
for k := range m { for k := range m {
delete(m, k) delete(m, k)
} }
} }
// MetaAccess tracks resources metadata.
var MetaAccess = NewMeta()
// Meta represents available resource metas. // Meta represents available resource metas.
type Meta struct { type Meta struct {
resMetas ResourceMetas resMetas ResourceMetas
@ -64,71 +66,40 @@ func NewMeta() *Meta {
return &Meta{resMetas: make(ResourceMetas)} return &Meta{resMetas: make(ResourceMetas)}
} }
// AccessorFor returns a client accessor for a resource if registered. func (m *Meta) Lookup(cmd string) *client.GVR {
// Otherwise it returns a generic accessor. m.mx.RLock()
// Customize here for non resource types or types with metrics or logs. defer m.mx.RUnlock()
func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) { for gvr, meta := range m.resMetas {
m := Accessors{ if slices.Contains(meta.ShortNames, cmd) {
client.NewGVR("workloads"): &Workload{}, return gvr
client.NewGVR("contexts"): &Context{}, }
client.NewGVR("containers"): &Container{}, if meta.Name == cmd || meta.SingularName == cmd || meta.Kind == cmd {
client.NewGVR("scans"): &ImageScan{}, return gvr
client.NewGVR("screendumps"): &ScreenDump{}, }
client.NewGVR("benchmarks"): &Benchmark{},
client.NewGVR("portforwards"): &PortForward{},
client.NewGVR("dir"): &Dir{},
client.NewGVR("v1/services"): &Service{},
client.NewGVR("v1/pods"): &Pod{},
client.NewGVR("v1/nodes"): &Node{},
client.NewGVR("v1/namespaces"): &Namespace{},
client.NewGVR("v1/configmaps"): &ConfigMap{},
client.NewGVR("v1/secrets"): &Secret{},
client.NewGVR("apps/v1/deployments"): &Deployment{},
client.NewGVR("apps/v1/daemonsets"): &DaemonSet{},
client.NewGVR("apps/v1/statefulsets"): &StatefulSet{},
client.NewGVR("apps/v1/replicasets"): &ReplicaSet{},
client.NewGVR("batch/v1/cronjobs"): &CronJob{},
client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
client.NewGVR("batch/v1/jobs"): &Job{},
client.NewGVR("helm"): &HelmChart{},
client.NewGVR("helm-history"): &HelmHistory{},
client.NewGVR("apiextensions.k8s.io/v1/customresourcedefinitions"): &CustomResourceDefinition{},
} }
r, ok := m[gvr] return client.NoGVR
if !ok {
r = new(Scaler)
slog.Debug("No DAO registry entry. Using generics!", slogs.GVR, gvr)
}
r.Init(f, gvr)
return r, nil
} }
// RegisterMeta registers a new resource meta object. // RegisterMeta registers a new resource meta object.
func (m *Meta) RegisterMeta(gvr string, res metav1.APIResource) { func (m *Meta) RegisterMeta(gvr string, res *metav1.APIResource) {
m.mx.Lock() m.mx.Lock()
defer m.mx.Unlock() defer m.mx.Unlock()
m.resMetas[client.NewGVR(gvr)] = res m.resMetas[client.NewGVR(gvr)] = res
} }
// AllGVRs returns all cluster resources. // AllGVRs returns all sorted cluster resources.
func (m *Meta) AllGVRs() client.GVRs { func (m *Meta) AllGVRs() client.GVRs {
m.mx.RLock() m.mx.RLock()
defer m.mx.RUnlock() defer m.mx.RUnlock()
kk := slices.Collect(maps.Keys(m.resMetas))
kk := make(client.GVRs, 0, len(m.resMetas)) return client.GVRs(kk)
for k := range m.resMetas {
kk = append(kk, k)
}
sort.Sort(kk)
return kk
} }
// GVK2GVR convert gvk to gvr // GVK2GVR convert gvk to gvr
func (m *Meta) GVK2GVR(gv schema.GroupVersion, kind string) (client.GVR, bool, bool) { func (m *Meta) GVK2GVR(gv schema.GroupVersion, kind string) (*client.GVR, bool, bool) {
m.mx.RLock() m.mx.RLock()
defer m.mx.RUnlock() defer m.mx.RUnlock()
@ -142,36 +113,36 @@ func (m *Meta) GVK2GVR(gv schema.GroupVersion, kind string) (client.GVR, bool, b
} }
// 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()
defer m.mx.RUnlock() defer m.mx.RUnlock()
meta, ok := m.resMetas[gvr] if meta, ok := m.resMetas[gvr]; ok {
if !ok { return meta, nil
return metav1.APIResource{}, fmt.Errorf("no resource meta defined for %q", gvr)
} }
return meta, nil
return new(metav1.APIResource), fmt.Errorf("no resource meta defined for\n %q", gvr)
} }
// IsCRD checks if resource represents a CRD // IsCRD checks if resource represents a CRD
func IsCRD(r metav1.APIResource) bool { func IsCRD(r *metav1.APIResource) bool {
return slices.Contains(r.Categories, crdCat) return slices.Contains(r.Categories, crdCat)
} }
// IsK8sMeta checks for non resource meta. // IsK8sMeta checks for non resource meta.
func IsK8sMeta(m metav1.APIResource) bool { func IsK8sMeta(m *metav1.APIResource) bool {
return !slices.ContainsFunc(m.Categories, func(category string) bool { return !slices.ContainsFunc(m.Categories, func(category string) bool {
return category == k9sCat || category == helmCat return category == k9sCat || category == helmCat
}) })
} }
// IsK9sMeta checks for non resource meta. // IsK9sMeta checks for non resource meta.
func IsK9sMeta(m metav1.APIResource) bool { func IsK9sMeta(m *metav1.APIResource) bool {
return slices.Contains(m.Categories, k9sCat) return slices.Contains(m.Categories, k9sCat)
} }
// IsScalable check if the resource can be scaled // IsScalable check if the resource can be scaled
func IsScalable(m metav1.APIResource) bool { func IsScalable(m *metav1.APIResource) bool {
return slices.Contains(m.Categories, scaleCat) return slices.Contains(m.Categories, scaleCat)
} }
@ -201,7 +172,7 @@ func loadNonResource(m ResourceMetas) {
} }
func loadK9s(m ResourceMetas) { func loadK9s(m ResourceMetas) {
m[client.NewGVR("workloads")] = metav1.APIResource{ m[client.WkGVR] = &metav1.APIResource{
Name: "workloads", Name: "workloads",
Kind: "Workload", Kind: "Workload",
SingularName: "workload", SingularName: "workload",
@ -209,48 +180,48 @@ func loadK9s(m ResourceMetas) {
ShortNames: []string{"wk"}, ShortNames: []string{"wk"},
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("pulses")] = metav1.APIResource{ m[client.PuGVR] = &metav1.APIResource{
Name: "pulses", Name: "pulses",
Kind: "Pulse", Kind: "Pulse",
SingularName: "pulses", SingularName: "pulse",
ShortNames: []string{"hz", "pu"}, ShortNames: []string{"hz", "pu"},
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("dir")] = metav1.APIResource{ m[client.DirGVR] = &metav1.APIResource{
Name: "dir", Name: "dirs",
Kind: "Dir", Kind: "Dir",
SingularName: "dir", SingularName: "dir",
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("xrays")] = metav1.APIResource{ m[client.XGVR] = &metav1.APIResource{
Name: "xray", Name: "xrays",
Kind: "XRays", Kind: "XRays",
SingularName: "xray", SingularName: "xray",
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("references")] = metav1.APIResource{ m[client.RefGVR] = &metav1.APIResource{
Name: "references", Name: "references",
Kind: "References", Kind: "References",
SingularName: "reference", SingularName: "reference",
Verbs: []string{}, Verbs: []string{},
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("aliases")] = metav1.APIResource{ m[client.AliGVR] = &metav1.APIResource{
Name: "aliases", Name: "aliases",
Kind: "Aliases", Kind: "Aliases",
SingularName: "alias", SingularName: "alias",
Verbs: []string{}, Verbs: []string{},
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("contexts")] = metav1.APIResource{ m[client.CtGVR] = &metav1.APIResource{
Name: "contexts", Name: client.CtGVR.String(),
Kind: "Contexts", Kind: "Contexts",
SingularName: "context", SingularName: "context",
ShortNames: []string{"ctx"}, ShortNames: []string{"ctx"},
Verbs: []string{}, Verbs: []string{},
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("screendumps")] = metav1.APIResource{ m[client.SdGVR] = &metav1.APIResource{
Name: "screendumps", Name: "screendumps",
Kind: "ScreenDumps", Kind: "ScreenDumps",
SingularName: "screendump", SingularName: "screendump",
@ -258,7 +229,7 @@ func loadK9s(m ResourceMetas) {
Verbs: []string{"delete"}, Verbs: []string{"delete"},
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("benchmarks")] = metav1.APIResource{ m[client.BeGVR] = &metav1.APIResource{
Name: "benchmarks", Name: "benchmarks",
Kind: "Benchmarks", Kind: "Benchmarks",
SingularName: "benchmark", SingularName: "benchmark",
@ -266,7 +237,7 @@ func loadK9s(m ResourceMetas) {
Verbs: []string{"delete"}, Verbs: []string{"delete"},
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("portforwards")] = metav1.APIResource{ m[client.PfGVR] = &metav1.APIResource{
Name: "portforwards", Name: "portforwards",
Namespaced: true, Namespaced: true,
Kind: "PortForwards", Kind: "PortForwards",
@ -275,14 +246,14 @@ func loadK9s(m ResourceMetas) {
Verbs: []string{"delete"}, Verbs: []string{"delete"},
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("containers")] = metav1.APIResource{ m[client.CoGVR] = &metav1.APIResource{
Name: "containers", Name: "containers",
Kind: "Containers", Kind: "Containers",
SingularName: "container", SingularName: "container",
Verbs: []string{}, Verbs: []string{},
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("scans")] = metav1.APIResource{ m[client.ScnGVR] = &metav1.APIResource{
Name: "scans", Name: "scans",
Kind: "Scans", Kind: "Scans",
SingularName: "scan", SingularName: "scan",
@ -292,14 +263,14 @@ func loadK9s(m ResourceMetas) {
} }
func loadHelm(m ResourceMetas) { func loadHelm(m ResourceMetas) {
m[client.NewGVR("helm")] = metav1.APIResource{ m[client.HmGVR] = &metav1.APIResource{
Name: "helm", Name: "helm",
Kind: "Helm", Kind: "Helm",
Namespaced: true, Namespaced: true,
Verbs: []string{"delete"}, Verbs: []string{"delete"},
Categories: []string{helmCat}, Categories: []string{helmCat},
} }
m[client.NewGVR("helm-history")] = metav1.APIResource{ m[client.HmhGVR] = &metav1.APIResource{
Name: "history", Name: "history",
Kind: "History", Kind: "History",
Namespaced: true, Namespaced: true,
@ -309,23 +280,23 @@ func loadHelm(m ResourceMetas) {
} }
func loadRBAC(m ResourceMetas) { func loadRBAC(m ResourceMetas) {
m[client.NewGVR("rbac")] = metav1.APIResource{ m[client.RbacGVR] = &metav1.APIResource{
Name: "rbacs", Name: "rbacs",
Kind: "Rules", Kind: "Rules",
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("policy")] = metav1.APIResource{ m[client.PolGVR] = &metav1.APIResource{
Name: "policies", Name: "policies",
Kind: "Rules", Kind: "Rules",
Namespaced: true, Namespaced: true,
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("users")] = metav1.APIResource{ m[client.UsrGVR] = &metav1.APIResource{
Name: "users", Name: "users",
Kind: "User", Kind: "User",
Categories: []string{k9sCat}, Categories: []string{k9sCat},
} }
m[client.NewGVR("groups")] = metav1.APIResource{ m[client.GrpGVR] = &metav1.APIResource{
Name: "groups", Name: "groups",
Kind: "Group", Kind: "Group",
Categories: []string{k9sCat}, Categories: []string{k9sCat},
@ -333,7 +304,7 @@ func loadRBAC(m ResourceMetas) {
} }
func loadPreferred(f Factory, m ResourceMetas) error { func loadPreferred(f Factory, m ResourceMetas) error {
if f.Client() == nil || !f.Client().ConnectionOK() { if f == nil || f.Client() == nil || !f.Client().ConnectionOK() {
slog.Error("Load cluster resources - No API server connection") slog.Error("Load cluster resources - No API server connection")
return nil return nil
} }
@ -347,7 +318,8 @@ func loadPreferred(f Factory, m ResourceMetas) error {
slog.Debug("Failed to load preferred resources", slogs.Error, err) slog.Debug("Failed to load preferred resources", slogs.Error, err)
} }
for _, r := range rr { for _, r := range rr {
for _, res := range r.APIResources { for i := range r.APIResources {
res := r.APIResources[i]
gvr := client.FromGVAndR(r.GroupVersion, res.Name) gvr := client.FromGVAndR(r.GroupVersion, res.Name)
if isDeprecated(gvr) { if isDeprecated(gvr) {
continue continue
@ -359,7 +331,7 @@ func loadPreferred(f Factory, m ResourceMetas) error {
if !isStandardGroup(r.GroupVersion) { if !isStandardGroup(r.GroupVersion) {
res.Categories = append(res.Categories, crdCat) res.Categories = append(res.Categories, crdCat)
} }
m[gvr] = res m[gvr] = &res
} }
} }
@ -370,21 +342,22 @@ func isStandardGroup(gv string) bool {
return stdGroups.Has(gv) || strings.Contains(gv, ".k8s.io") return stdGroups.Has(gv) || strings.Contains(gv, ".k8s.io")
} }
var deprecatedGVRs = sets.New[client.GVR]( var deprecatedGVRs = sets.New(
client.NewGVR("v1/events"),
client.NewGVR("extensions/v1beta1/ingresses"), client.NewGVR("extensions/v1beta1/ingresses"),
) )
func isDeprecated(gvr client.GVR) bool { func isDeprecated(gvr *client.GVR) bool {
return deprecatedGVRs.Has(gvr) return deprecatedGVRs.Has(gvr) || gvr.V() == ""
} }
// loadCRDs Wait for the cache to synced and then add some additional properties to CRD. // loadCRDs Wait for the cache to synced and then add some additional properties to CRD.
func loadCRDs(f Factory, m ResourceMetas) { func loadCRDs(f Factory, m ResourceMetas) {
if f.Client() == nil || !f.Client().ConnectionOK() { if f == nil || f.Client() == nil || !f.Client().ConnectionOK() {
return return
} }
oo, err := f.List(crdGVR, client.ClusterScope, true, labels.Everything()) oo, err := f.List(client.CrdGVR, client.ClusterScope, true, labels.Everything())
if err != nil { if err != nil {
slog.Warn("CRDs load Fail", slogs.Error, err) slog.Warn("CRDs load Fail", slogs.Error, err)
return return
@ -397,125 +370,13 @@ func loadCRDs(f Factory, m ResourceMetas) {
slog.Error("CRD conversion failed", slogs.Error, err) slog.Error("CRD conversion failed", slogs.Error, err)
continue continue
} }
gvr, version, ok := newGVRFromCRD(&crd) for gvr, version := range client.NewGVRFromCRD(&crd) {
if !ok { if meta, ok := m[gvr]; ok && version.Subresources != nil && version.Subresources.Scale != nil {
continue if !slices.Contains(meta.Categories, scaleCat) {
} meta.Categories = append(meta.Categories, scaleCat)
m[gvr] = meta
if meta, ok := m[gvr]; ok && version.Subresources != nil && version.Subresources.Scale != nil { }
if !slices.Contains(meta.Categories, scaleCat) {
meta.Categories = append(meta.Categories, scaleCat)
m[gvr] = meta
} }
} }
} }
} }
func newGVRFromCRD(crd *apiext.CustomResourceDefinition) (client.GVR, apiext.CustomResourceDefinitionVersion, bool) {
for _, v := range crd.Spec.Versions {
if v.Served && !v.Deprecated {
return client.NewGVRFromMeta(metav1.APIResource{
Kind: crd.Spec.Names.Kind,
Group: crd.Spec.Group,
Name: crd.Spec.Names.Plural,
Version: v.Name,
}), v, true
}
}
return client.GVR{}, apiext.CustomResourceDefinitionVersion{}, false
}
func extractMeta(o runtime.Object) (metav1.APIResource, []error) {
var (
m metav1.APIResource
errs []error
)
crd, ok := o.(*unstructured.Unstructured)
if !ok {
return m, append(errs, fmt.Errorf("expected unstructured, but got %T", o))
}
var spec map[string]interface{}
spec, errs = extractMap(crd.Object, "spec", errs)
var meta map[string]interface{}
meta, errs = extractMap(crd.Object, "metadata", errs)
m.Name, errs = extractStr(meta, "name", errs)
m.Group, errs = extractStr(spec, "group", errs)
versions, errs := extractSlice(spec, "versions", errs)
if len(versions) > 0 {
m.Version = versions[0]
}
var scope string
scope, errs = extractStr(spec, "scope", errs)
m.Namespaced = isNamespaced(scope)
var names map[string]interface{}
names, errs = extractMap(spec, "names", errs)
m.Kind, errs = extractStr(names, "kind", errs)
m.SingularName, errs = extractStr(names, "singular", errs)
m.Name, errs = extractStr(names, "plural", errs)
m.ShortNames, errs = extractSlice(names, "shortNames", errs)
return m, errs
}
func isNamespaced(scope string) bool {
return scope == "Namespaced"
}
func extractSlice(m map[string]interface{}, n string, errs []error) ([]string, []error) {
if m[n] == nil {
return nil, errs
}
s, ok := m[n].([]string)
if ok {
return s, errs
}
ii, ok := m[n].([]interface{})
if !ok {
return s, append(errs, fmt.Errorf("failed to extract slice %s -- %#v", n, m))
}
ss := make([]string, 0, len(ii))
for _, name := range ii {
switch o := name.(type) {
case string:
ss = append(ss, o)
case map[string]interface{}:
s, ok := o["name"].(string)
if ok {
ss = append(ss, s)
} else {
errs = append(errs, fmt.Errorf("unable to find key %q in map", n))
}
default:
errs = append(errs, fmt.Errorf("unknown field type %t for key %q", o, n))
}
}
return ss, errs
}
func extractStr(m map[string]interface{}, n string, errs []error) (string, []error) {
s, ok := m[n].(string)
if !ok {
return s, append(errs, fmt.Errorf("failed to extract string %s", n))
}
return s, errs
}
func extractMap(m map[string]interface{}, n string, errs []error) (map[string]interface{}, []error) {
v, ok := m[n].(map[string]interface{})
if !ok {
return v, append(errs, fmt.Errorf("failed to extract field %s", n))
}
return v, errs
}

View File

@ -4,100 +4,78 @@
package dao package dao
import ( import (
"encoding/json" "errors"
"fmt"
"os"
"testing" "testing"
"github.com/derailed/k9s/internal/client"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
) )
func TestExtractMeta(t *testing.T) { func TestMetaFor(t *testing.T) {
c := load(t, "dr")
m, ee := extractMeta(c)
assert.Equal(t, 0, len(ee))
assert.Equal(t, "destinationrules", m.Name)
assert.Equal(t, "destinationrule", m.SingularName)
assert.Equal(t, "DestinationRule", m.Kind)
assert.Equal(t, "networking.istio.io", m.Group)
assert.Equal(t, "v1alpha3", m.Version)
assert.Equal(t, true, m.Namespaced)
assert.Equal(t, []string{"dr"}, m.ShortNames)
var vv metav1.Verbs
assert.Equal(t, vv, m.Verbs)
}
func TestExtractSlice(t *testing.T) {
uu := map[string]struct { uu := map[string]struct {
m map[string]interface{} gvr *client.GVR
n string err error
nn []string e metav1.APIResource
ee []error
}{ }{
"plain": { "xray-gvr": {
m: map[string]interface{}{"shortNames": []string{"a", "b", "c"}}, gvr: client.XGVR,
n: "shortNames", e: metav1.APIResource{
nn: []string{"a", "b", "c"}, Name: "xrays",
Kind: "XRays",
SingularName: "xray",
Categories: []string{k9sCat},
},
}, },
"empty": {
m: map[string]interface{}{}, "xray": {
n: "shortNames", gvr: client.NewGVR("xrays"),
e: metav1.APIResource{
Name: "xrays",
Kind: "XRays",
SingularName: "xray",
Categories: []string{k9sCat},
},
},
"policy": {
gvr: client.NewGVR("policy"),
e: metav1.APIResource{
Name: "policies",
Kind: "Rules",
Namespaced: true,
Categories: []string{k9sCat},
},
},
"helm": {
gvr: client.NewGVR("helm"),
e: metav1.APIResource{
Name: "helm",
Kind: "Helm",
Namespaced: true,
Verbs: []string{"delete"},
Categories: []string{helmCat},
},
},
"toast": {
gvr: client.NewGVR("blah"),
err: errors.New("no resource meta defined for\n \"blah\""),
}, },
} }
var ee []error m := NewMeta()
require.NoError(t, m.LoadResources(nil))
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) {
ss, e := extractSlice(u.m, u.n, ee) meta, err := m.MetaFor(u.gvr)
assert.Equal(t, u.ee, e) assert.Equal(t, u.err, err)
assert.Equal(t, u.nn, ss) if err == nil {
assert.Equal(t, &u.e, meta)
}
}) })
} }
} }
func TestExtractString(t *testing.T) {
uu := map[string]struct {
m map[string]interface{}
n string
s string
ee []error
}{
"plain": {
m: map[string]interface{}{"blee": "fred"},
n: "blee",
s: "fred",
},
"missing": {
m: map[string]interface{}{},
n: "blee",
ee: []error{fmt.Errorf("failed to extract string blee")},
},
}
var ee []error
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
as, ae := extractStr(u.m, u.n, ee)
assert.Equal(t, u.ee, ae)
assert.Equal(t, u.s, as)
})
}
}
// Helpers...
func load(t *testing.T, n string) *unstructured.Unstructured {
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
assert.Nil(t, err)
var o unstructured.Unstructured
err = json.Unmarshal(raw, &o)
assert.Nil(t, err)
return &o
}

View File

@ -33,12 +33,12 @@ func (r *Resource) List(ctx context.Context, ns string) ([]runtime.Object, error
} }
} }
return r.getFactory().List(r.gvrStr(), ns, false, lsel) return r.getFactory().List(r.gvr, ns, false, lsel)
} }
// Get returns a resource instance if found, else an error. // Get returns a resource instance if found, else an error.
func (r *Resource) Get(_ context.Context, path string) (runtime.Object, error) { func (r *Resource) Get(_ context.Context, path string) (runtime.Object, error) {
return r.getFactory().Get(r.gvrStr(), path, true, labels.Everything()) return r.getFactory().Get(r.gvr, path, true, labels.Everything())
} }
// ToYAML returns a resource yaml. // ToYAML returns a resource yaml.

View File

@ -65,11 +65,12 @@ func (r *RestMapper) resourceFor(resourceArg string) (schema.GroupVersionResourc
gvr, err = mapper.ResourceFor(gr.WithVersion("")) gvr, err = mapper.ResourceFor(gr.WithVersion(""))
if err != nil { if err != nil {
if len(gr.Group) == 0 { if gr.Group == "" {
return gvr, fmt.Errorf("the server doesn't have a resource type '%s'", gr.Resource) return gvr, fmt.Errorf("the server doesn't have a resource type '%s'", gr.Resource)
} }
return gvr, fmt.Errorf("the server doesn't have a resource type '%s' in group '%s'", gr.Resource, gr.Group) return gvr, fmt.Errorf("the server doesn't have a resource type '%s' in group '%s'", gr.Resource, gr.Group)
} }
return gvr, nil return gvr, nil
} }

Some files were not shown because too many files have changed in this diff Show More