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-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."
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 }}

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"
run:
concurrency: 8
allow-parallel-runners: true
# timeout for analysis, e.g. 30s, 5m, default is 1m
timeout: 5m
@ -10,137 +10,305 @@ run:
issues-exit-code: 1
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:
enable:
- gci
- gofmt
# - gofumpt
- goimports
# - golines
linters:
# disable-all: true
enable:
- sloglint
settings:
gofmt:
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
goimports:
local-prefixes:
- github.com/golangci/golangci-lint/v2
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
- \\.(generated\\.deepcopy|pb)\\.go$
- 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
settings:
gocyclo:
min-complexity: 35
# linters:
# default: none
# enable:
# - sloglint
govet:
enable:
- nilness
# exclusions:
# generated: lax
# paths:
# - third_party$
# - builtin$
# - examples$
# - \\.(generated\\.deepcopy|pb)\\.go$
goimports:
local-prefixes: github.com/derailed/k9s
# settings:
# gocyclo:
# min-complexity: 35
unused:
parameters-are-used: true
local-variables-are-used: true
field-writes-are-uses: true
post-statements-are-reads: true
exported-fields-are-used: true
generated-is-used: true
# govet:
# enable:
# - nilness
goheader:
values:
regexp:
PROJECT: 'K9s'
template: |-
SPDX-License-Identifier: Apache-2.0
Copyright Authors of {{ PROJECT }}
# goimports:
# local-prefixes: github.com/derailed/k9s
gosec:
includes:
- G402
# unused:
# parameters-are-used: true
# local-variables-are-used: true
# field-writes-are-uses: true
# post-statements-are-reads: true
# exported-fields-are-used: true
# generated-is-used: true
sloglint:
# Enforce not mixing key-value pairs and attributes.
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-mixed-arguments
# Default: true
no-mixed-args: true
# Enforce using key-value pairs only (overrides no-mixed-args, incompatible with attr-only).
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#key-value-pairs-only
# Default: false
kv-only: true
# Enforce using attributes only (overrides no-mixed-args, incompatible with kv-only).
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#attributes-only
# 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
# goheader:
# values:
# regexp:
# PROJECT: 'K9s'
# template: |-
# SPDX-License-Identifier: Apache-2.0
# Copyright Authors of {{ PROJECT }}
# gosec:
# includes:
# - G402
issues:
# default is true. Enables skipping of directories:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
# exclude-dirs-use-default: true
# issues:
# Excluding configuration per-path, per-linter, per-text and per-source
# exclude-rules:
# - linters: [staticcheck]
# text: "SA1019" # this is rule for deprecated method
# # default is true. Enables skipping of directories:
# # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
# # exclude-dirs-use-default: true
# - linters: [staticcheck]
# text: "SA9003: empty branch"
# # Excluding configuration per-path, per-linter, per-text and per-source
# # exclude-rules:
# # - linters: [staticcheck]
# # text: "SA1019" # this is rule for deprecated method
# - linters: [staticcheck]
# text: "SA2001: empty critical section"
# # - linters: [staticcheck]
# # text: "SA9003: empty branch"
# - linters: [err113]
# text: "do not define dynamic errors, use wrapped static errors instead" # This rule to avoid opinionated check fmt.Errorf("text")
# # Skip goimports check on generated files
# - path: \\.(generated\\.deepcopy|pb)\\.go$
# linters:
# - goimports
# # Skip goheader check on files imported and modified from upstream k8s
# - path: "pkg/ipam/(cidrset|service)/.+\\.go"
# linters:
# - goheader
# # - linters: [staticcheck]
# # text: "SA2001: empty critical section"
# # - linters: [err113]
# # text: "do not define dynamic errors, use wrapped static errors instead" # This rule to avoid opinionated check fmt.Errorf("text")
# # # Skip goimports check on generated files
# # - path: \\.(generated\\.deepcopy|pb)\\.go$
# # linters:
# # - goimports
# # # 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
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
endif
VERSION ?= v0.40.10
VERSION ?= v0.50.0
IMG_NAME := derailed/k9s
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 {
return err
}

View File

@ -57,7 +57,7 @@ func init() {
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}
})
@ -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 {
return err
}
@ -378,7 +378,7 @@ func initK8sFlagCompletion() {
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)
if c, err := client.InitConnection(conn, slog.Default()); err == nil {
if nss, err := c.ValidNamespaceNames(); err == nil {
@ -391,7 +391,7 @@ func initK8sFlagCompletion() {
}
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)
cfg, err := conn.RawConfig()
if err != nil {

View File

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

2
go.mod
View File

@ -15,6 +15,7 @@ require (
github.com/fsnotify/fsnotify v1.8.0
github.com/fvbommel/sortorder v1.1.0
github.com/go-errors/errors v1.5.1
github.com/itchyny/gojq v0.12.17
github.com/lmittmann/tint v1.0.7
github.com/mattn/go-colorable v0.1.14
github.com/mattn/go-runewidth v0.0.16
@ -192,6 +193,7 @@ require (
github.com/huandu/xstrings v1.5.0 // indirect
github.com/iancoleman/strcase v0.3.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/jinzhu/copier v0.4.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.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
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/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=

View File

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

View File

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

View File

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

View File

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

View File

@ -11,11 +11,12 @@ import (
"github.com/derailed/k9s/internal/slogs"
"github.com/fvbommel/sortorder"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var NoGVR = GVR{}
var NoGVR = &GVR{}
// GVR represents a kubernetes resource schema as a string.
// Format is group/version/resources:subresource.
@ -23,12 +24,29 @@ type GVR struct {
raw, g, v, r, sr string
}
// NewGVR builds a new gvr from a group, version, resource.
func NewGVR(gvr string) GVR {
var g, v, r, sr string
type gvrCache map[string]*GVR
tokens := strings.Split(gvr, ":")
raw := gvr
func (c gvrCache) add(gvr *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 {
raw, sr = tokens[0], tokens[1]
}
@ -41,34 +59,62 @@ func NewGVR(gvr string) GVR {
case 1:
r = tokens[0]
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.
func NewGVRFromMeta(a metav1.APIResource) GVR {
return GVR{
raw: path.Join(a.Group, a.Version, a.Name),
g: a.Group,
v: a.Version,
r: a.Name,
func NewGVRFromMeta(a *metav1.APIResource) *GVR {
return NewGVR(path.Join(a.Group, a.Version, a.Name))
}
// NewGVRFromCRD builds a gvr from a custom resource definition.
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.
func FromGVAndR(gv, r string) GVR {
func FromGVAndR(gv, r string) *GVR {
return NewGVR(path.Join(gv, r))
}
// 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)
}
// 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 == "" {
return g.r
}
@ -77,17 +123,17 @@ func (g GVR) AsResourceName() string {
}
// SubResource returns a sub resource if available.
func (g GVR) SubResource() string {
func (g *GVR) SubResource() string {
return g.sr
}
// String returns gvr as string.
func (g GVR) String() string {
func (g *GVR) String() string {
return g.raw
}
// GV returns the group version scheme representation.
func (g GVR) GV() schema.GroupVersion {
func (g *GVR) GV() schema.GroupVersion {
return schema.GroupVersion{
Group: g.g,
Version: g.v,
@ -95,7 +141,7 @@ func (g GVR) GV() schema.GroupVersion {
}
// GVK returns a full schema representation.
func (g GVR) GVK() schema.GroupVersionKind {
func (g *GVR) GVK() schema.GroupVersionKind {
return schema.GroupVersionKind{
Group: g.G(),
Version: g.V(),
@ -104,7 +150,7 @@ func (g GVR) GVK() schema.GroupVersionKind {
}
// GVR returns a full schema representation.
func (g GVR) GVR() schema.GroupVersionResource {
func (g *GVR) GVR() schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: g.G(),
Version: g.V(),
@ -113,7 +159,7 @@ func (g GVR) GVR() schema.GroupVersionResource {
}
// GVSub returns group vervion sub path.
func (g GVR) GVSub() string {
func (g *GVR) GVSub() string {
if g.G() == "" {
return g.V()
}
@ -122,7 +168,7 @@ func (g GVR) GVSub() string {
}
// GR returns a full schema representation.
func (g GVR) GR() *schema.GroupResource {
func (g *GVR) GR() *schema.GroupResource {
return &schema.GroupResource{
Group: g.G(),
Resource: g.R(),
@ -130,32 +176,32 @@ func (g GVR) GR() *schema.GroupResource {
}
// V returns the resource version.
func (g GVR) V() string {
func (g *GVR) V() string {
return g.v
}
// RG returns the resource and group.
func (g GVR) RG() (string, string) {
func (g *GVR) RG() (resource, group string) {
return g.r, g.g
}
// R returns the resource name.
func (g GVR) R() string {
func (g *GVR) R() string {
return g.r
}
// G returns the resource group name.
func (g GVR) G() string {
func (g *GVR) G() string {
return g.g
}
// 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"
}
// GVRs represents a collection of gvr.
type GVRs []GVR
type GVRs []*GVR
// Len returns the list size.
func (g GVRs) Len() int {

View File

@ -15,15 +15,15 @@ import (
func TestGVRSort(t *testing.T) {
gg := client.GVRs{
client.NewGVR("v1/pods"),
client.NewGVR("v1/services"),
client.NewGVR("apps/v1/deployments"),
client.PodGVR,
client.SvcGVR,
client.DpGVR,
}
sort.Sort(gg)
assert.Equal(t, client.GVRs{
client.NewGVR("v1/pods"),
client.NewGVR("v1/services"),
client.NewGVR("apps/v1/deployments"),
client.PodGVR,
client.SvcGVR,
client.DpGVR,
}, gg)
}
@ -54,9 +54,9 @@ func TestGVR(t *testing.T) {
gvr string
e schema.GroupVersionResource
}{
"full": {"apps/v1/deployments", schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}},
"core": {"v1/pods", schema.GroupVersionResource{Version: "v1", Resource: "pods"}},
"bork": {"users", schema.GroupVersionResource{Resource: "users"}},
"full": {client.DpGVR.String(), schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}},
"core": {client.PodGVR.String(), schema.GroupVersionResource{Version: "v1", Resource: "pods"}},
"bork": {client.UsrGVR.String(), schema.GroupVersionResource{Resource: "users"}},
}
for k := range uu {
@ -72,9 +72,9 @@ func TestAsGV(t *testing.T) {
gvr string
e schema.GroupVersion
}{
"full": {"apps/v1/deployments", schema.GroupVersion{Group: "apps", Version: "v1"}},
"core": {"v1/pods", schema.GroupVersion{Version: "v1"}},
"bork": {"users", schema.GroupVersion{}},
"full": {client.DpGVR.String(), schema.GroupVersion{Group: "apps", Version: "v1"}},
"core": {client.PodGVR.String(), schema.GroupVersion{Version: "v1"}},
"bork": {client.UsrGVR.String(), schema.GroupVersion{}},
}
for k := range uu {
@ -90,8 +90,8 @@ func TestNewGVR(t *testing.T) {
g, v, r string
e string
}{
"full": {"apps", "v1", "deployments", "apps/v1/deployments"},
"core": {"", "v1", "pods", "v1/pods"},
"full": {"apps", "v1", "deployments", client.DpGVR.String()},
"core": {"", "v1", "pods", client.PodGVR.String()},
}
for k := range uu {
@ -107,9 +107,9 @@ func TestGVRAsResourceName(t *testing.T) {
gvr string
e string
}{
"full": {"apps/v1/deployments", "deployments.v1.apps"},
"core": {"v1/pods", "pods"},
"k9s": {"users", "users"},
"full": {client.DpGVR.String(), "deployments.v1.apps"},
"core": {client.PodGVR.String(), "pods"},
"k9s": {client.UsrGVR.String(), "users"},
"empty": {"", ""},
}
@ -126,9 +126,9 @@ func TestToR(t *testing.T) {
gvr string
e string
}{
"full": {"apps/v1/deployments", "deployments"},
"core": {"v1/pods", "pods"},
"k9s": {"users", "users"},
"full": {client.DpGVR.String(), "deployments"},
"core": {client.PodGVR.String(), "pods"},
"k9s": {client.UsrGVR.String(), "users"},
"empty": {"", ""},
}
@ -145,9 +145,9 @@ func TestToG(t *testing.T) {
gvr string
e string
}{
"full": {"apps/v1/deployments", "apps"},
"core": {"v1/pods", ""},
"k9s": {"users", ""},
"full": {client.DpGVR.String(), "apps"},
"core": {client.PodGVR.String(), ""},
"k9s": {client.UsrGVR.String(), ""},
"empty": {"", ""},
}
@ -164,9 +164,9 @@ func TestToV(t *testing.T) {
gvr string
e string
}{
"full": {"apps/v1/deployments", "v1"},
"full": {client.DpGVR.String(), "v1"},
"core": {"v1beta1/pods", "v1beta1"},
"k9s": {"users", ""},
"k9s": {client.UsrGVR.String(), ""},
"empty": {"", ""},
}
@ -182,9 +182,9 @@ func TestToString(t *testing.T) {
uu := map[string]struct {
gvr string
}{
"full": {"apps/v1/deployments"},
"full": {client.DpGVR.String()},
"core": {"v1beta1/pods"},
"k9s": {"users"},
"k9s": {client.UsrGVR.String()},
"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 {
u := uu[k]
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 {
u := uu[k]
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.
func Namespaced(p string) (string, string) {
ns, n := path.Split(p)
func Namespaced(p string) (ns, name string) {
ns, name = path.Split(p)
return strings.Trim(ns, "/"), n
return strings.Trim(ns, "/"), 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
}
@ -72,7 +72,7 @@ func FQN(ns, n string) string {
}
// MetaFQN returns a fully qualified resource name.
func MetaFQN(m metav1.ObjectMeta) string {
func MetaFQN(m *metav1.ObjectMeta) string {
if m.Namespace == "" {
return FQN(ClusterScope, m.Name)
}
@ -90,6 +90,9 @@ func mustHomeDir() 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, "_")
}

View File

@ -20,8 +20,6 @@ import (
const (
mxCacheSize = 100
mxCacheExpiry = 1 * time.Minute
podMXGVR = "metrics.k8s.io/v1beta1/pods"
nodeMXGVR = "metrics.k8s.io/v1beta1/nodes"
)
// MetricsDial tracks global metric server handle.
@ -57,22 +55,22 @@ func NewMetricsServer(c Connection) *MetricsServer {
}
// 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 {
return fmt.Errorf("invalid node or node metrics lists")
}
nodeMetrics := make(NodesMetrics, len(nos.Items))
for _, no := range nos.Items {
nodeMetrics[no.Name] = NodeMetrics{
AllocatableCPU: no.Status.Allocatable.Cpu().MilliValue(),
AllocatableMEM: no.Status.Allocatable.Memory().Value(),
for i := range nos.Items {
nodeMetrics[nos.Items[i].Name] = NodeMetrics{
AllocatableCPU: nos.Items[i].Status.Allocatable.Cpu().MilliValue(),
AllocatableMEM: nos.Items[i].Status.Allocatable.Memory().Value(),
}
}
for _, mx := range nmx.Items {
if node, ok := nodeMetrics[mx.Name]; ok {
node.CurrentCPU = mx.Usage.Cpu().MilliValue()
node.CurrentMEM = mx.Usage.Memory().Value()
nodeMetrics[mx.Name] = node
for i := range nmx.Items {
if node, ok := nodeMetrics[nmx.Items[i].Name]; ok {
node.CurrentCPU = nmx.Items[i].Usage.Cpu().MilliValue()
node.CurrentMEM = nmx.Items[i].Usage.Memory().Value()
nodeMetrics[nmx.Items[i].Name] = node
}
}
@ -88,7 +86,7 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
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() {
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.
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 {
return
}
for _, no := range nodes.Items {
mmx[no.Name] = NodeMetrics{
AllocatableCPU: no.Status.Allocatable.Cpu().MilliValue(),
AllocatableMEM: ToMB(no.Status.Allocatable.Memory().Value()),
AllocatableEphemeral: ToMB(no.Status.Allocatable.StorageEphemeral().Value()),
TotalCPU: no.Status.Capacity.Cpu().MilliValue(),
TotalMEM: ToMB(no.Status.Capacity.Memory().Value()),
TotalEphemeral: ToMB(no.Status.Capacity.StorageEphemeral().Value()),
for i := range nodes.Items {
mmx[nodes.Items[i].Name] = NodeMetrics{
AllocatableCPU: nodes.Items[i].Status.Allocatable.Cpu().MilliValue(),
AllocatableMEM: ToMB(nodes.Items[i].Status.Allocatable.Memory().Value()),
AllocatableEphemeral: ToMB(nodes.Items[i].Status.Allocatable.StorageEphemeral().Value()),
TotalCPU: nodes.Items[i].Status.Capacity.Cpu().MilliValue(),
TotalMEM: ToMB(nodes.Items[i].Status.Capacity.Memory().Value()),
TotalEphemeral: ToMB(nodes.Items[i].Status.Capacity.StorageEphemeral().Value()),
}
}
for _, c := range metrics.Items {
if mx, ok := mmx[c.Name]; ok {
mx.CurrentCPU = c.Usage.Cpu().MilliValue()
mx.CurrentMEM = ToMB(c.Usage.Memory().Value())
mx.AvailableCPU = mx.AllocatableCPU - mx.CurrentCPU
mx.AvailableMEM = mx.AllocatableMEM - mx.CurrentMEM
mmx[c.Name] = mx
for i := range metrics.Items {
mx, ok := mmx[metrics.Items[i].Name]
if !ok {
continue
}
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"
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
}
@ -182,7 +182,7 @@ func (m *MetricsServer) FetchNodeMetrics(ctx context.Context, n string) (*mv1bet
const msg = "user is not authorized to list node metrics"
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
}
@ -222,7 +222,7 @@ func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1be
if ns == NamespaceAll {
ns = BlankNamespace
}
if err := m.checkAccess(ns, podMXGVR, msg); err != nil {
if err := m.checkAccess(ns, PmxGVR, msg); err != nil {
return mx, err
}
@ -273,7 +273,7 @@ func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1be
if ns == NamespaceAll {
ns = BlankNamespace
}
if err := m.checkAccess(ns, podMXGVR, msg); err != nil {
if err := m.checkAccess(ns, PmxGVR, msg); err != nil {
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.
func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetrics) {
func (*MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetrics) {
if pods == nil {
return
}
// Compute all pod's containers metrics.
for _, p := range pods.Items {
for i := range pods.Items {
var mx PodMetrics
for _, c := range p.Containers {
for _, c := range pods.Items[i].Containers {
mx.CurrentCPU += c.Usage.Cpu().MilliValue()
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)
m.PodsMetrics(u.metrics, mmx)
assert.Equal(t, u.eSize, len(mmx))
assert.Len(t, mmx, u.eSize)
if u.eSize == 0 {
return
}
@ -104,7 +104,7 @@ func BenchmarkPodsMetrics(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
for range b.N {
m.PodsMetrics(&metrics, mmx)
}
}
@ -175,7 +175,7 @@ func TestNodesMetrics(t *testing.T) {
mmx := make(client.NodesMetrics)
m.NodesMetrics(u.nodes, u.metrics, mmx)
assert.Equal(t, u.eSize, len(mmx))
assert.Len(t, mmx, u.eSize)
if u.eSize == 0 {
return
}
@ -206,7 +206,7 @@ func BenchmarkNodesMetrics(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
for range b.N {
m.NodesMetrics(&nodes, &metrics, mmx)
}
}
@ -290,7 +290,7 @@ func BenchmarkClusterLoad(b *testing.B) {
var mx client.ClusterMetrics
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
for range b.N {
_ = 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.
type Authorizer interface {
// 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.

View File

@ -47,7 +47,7 @@ func TestHighlight(t *testing.T) {
for k := range uu {
u := uu[k]
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 (
"errors"
"fmt"
"io/fs"
"log/slog"
"os"
"sync"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/json"
"github.com/derailed/k9s/internal/slogs"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/util/sets"
)
// 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.
type ShortNames map[string][]string
type ShortNames map[*client.GVR][]string
// Aliases represents a collection of aliases.
type Aliases struct {
@ -35,29 +38,17 @@ func NewAliases() *Aliases {
}
}
func (a *Aliases) AliasesFor(s string) []string {
aa := make([]string, 0, 10)
func (a *Aliases) AliasesFor(gvr *client.GVR) sets.Set[string] {
a.mx.RLock()
defer a.mx.RUnlock()
for k, v := range a.Alias {
if v == s {
aa = append(aa, k)
ss := sets.New[string]()
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
}
@ -89,24 +80,27 @@ func (a *Aliases) Clear() {
}
// Get retrieves an alias.
func (a *Aliases) Get(k string) (string, bool) {
func (a *Aliases) Get(alias string) (*client.GVR, bool) {
a.mx.RLock()
defer a.mx.RUnlock()
v, ok := a.Alias[k]
return v, ok
gvr, ok := a.Alias[alias]
if ok && !gvr.IsK8sRes() {
if rgvr, found := a.Alias[gvr.String()]; found {
return rgvr, found
}
}
return gvr, ok
}
// 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()
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 {
if _, ok := a.Alias[alias]; !ok && alias != "" {
a.Alias[alias] = gvr
@ -117,12 +111,10 @@ func (a *Aliases) Define(gvr string, aliases ...string) {
// Load K9s aliases.
func (a *Aliases) Load(path string) error {
a.loadDefaultAliases()
f, err := EnsureAliasesCfgFile()
if err != nil {
slog.Error("Unable to gen config aliases", slogs.Error, err)
}
// load global alias file
if err := a.LoadFile(f); err != nil {
return err
@ -132,11 +124,18 @@ func (a *Aliases) Load(path string) error {
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.
func (a *Aliases) LoadFile(path string) error {
if path == "" {
return nil
}
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return nil
}
@ -149,23 +148,23 @@ func (a *Aliases) LoadFile(path string) error {
slog.Warn("Aliases validation failed", slogs.Error, err)
}
var aa Aliases
var aa aliases
if err := yaml.Unmarshal(bb, &aa); err != nil {
return err
}
a.mx.Lock()
defer a.mx.Unlock()
for k, v := range aa.Alias {
a.Alias[k] = v
for alias, cmd := range aa.Alias {
a.Alias[alias] = client.NewGVR(cmd)
}
return nil
}
func (a *Aliases) declare(key string, aliases ...string) {
a.Alias[key] = key
func (a *Aliases) declare(gvr *client.GVR, aliases ...string) {
a.Alias[gvr.String()] = gvr
for _, alias := range aliases {
a.Alias[alias] = key
a.Alias[alias] = gvr
}
}
@ -173,20 +172,20 @@ func (a *Aliases) loadDefaultAliases() {
a.mx.Lock()
defer a.mx.Unlock()
a.declare("help", "h", "?")
a.declare("quit", "q", "q!", "qa", "Q")
a.declare("aliases", "alias", "a")
a.declare("helm", "charts", "chart", "hm")
a.declare("dir", "d")
a.declare("contexts", "context", "ctx")
a.declare("users", "user", "usr")
a.declare("groups", "group", "grp")
a.declare("portforwards", "portforward", "pf")
a.declare("benchmarks", "benchmark", "bench")
a.declare("screendumps", "screendump", "sd")
a.declare("pulses", "pulse", "pu", "hz")
a.declare("xrays", "xray", "x")
a.declare("workloads", "workload", "wk")
a.declare(client.HlpGVR, "h", "?")
a.declare(client.QGVR, "q", "q!", "qa", "Q")
a.declare(client.AliGVR, "alias", "a")
a.declare(client.HmGVR, "charts", "chart", "hm")
a.declare(client.DirGVR, "dir", "d")
a.declare(client.CtGVR, "context", "ctx")
a.declare(client.UsrGVR, "user", "usr")
a.declare(client.GrpGVR, "group", "grp")
a.declare(client.PfGVR, "portforward", "pf")
a.declare(client.BeGVR, "benchmark", "bench")
a.declare(client.SdGVR, "screendump", "sd")
a.declare(client.PuGVR, "pulse", "pu", "hz")
a.declare(client.XGVR, "xray", "x")
a.declare(client.WkGVR, "workload", "wk")
}
// Save alias to disk.
@ -200,6 +199,10 @@ func (a *Aliases) SaveAliases(path string) error {
if err := data.EnsureDirPath(path, data.DefaultDirMod); err != nil {
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
import (
"fmt"
"maps"
"os"
"path"
"slices"
"testing"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/config/data"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAliasClear(t *testing.T) {
a := testAliases()
a.Clear()
assert.Equal(t, 0, len(a.Keys()))
assert.Empty(t, slices.Collect(maps.Keys(a.Alias)))
}
func TestAliasKeys(t *testing.T) {
a := testAliases()
kk := a.Keys()
slices.Sort(kk)
kk := maps.Keys(a.Alias)
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) {
a := testAliases()
ess := config.ShortNames{
"gvr1": []string{"a1", "a11"},
"gvr2": []string{"a2"},
"gvr3": []string{"a3"},
gvr1: []string{"a1", "a11"},
gvr2: []string{"a2"},
gvr3: []string{"a3"},
}
ss := a.ShortNames()
assert.Equal(t, len(ess), len(ss))
assert.Len(t, ss, len(ess))
for k, v := range ss {
v1, ok := ess[k]
assert.True(t, ok, fmt.Sprintf("missing: %q", k))
assert.True(t, ok, "missing: %q", k)
slices.Sort(v)
assert.Equal(t, v1, v)
}
@ -49,41 +50,41 @@ func TestAliasShortNames(t *testing.T) {
func TestAliasDefine(t *testing.T) {
type aliasDef struct {
cmd string
gvr *client.GVR
aliases []string
}
uu := map[string]struct {
aliases []aliasDef
registeredCommands map[string]string
registeredCommands map[string]*client.GVR
}{
"simple": {
aliases: []aliasDef{
{
cmd: "one",
gvr: client.NewGVR("one"),
aliases: []string{"blee", "duh"},
},
},
registeredCommands: map[string]string{
"blee": "one",
"duh": "one",
registeredCommands: map[string]*client.GVR{
"blee": client.NewGVR("one"),
"duh": client.NewGVR("one"),
},
},
"duplicates": {
aliases: []aliasDef{
{
cmd: "one",
gvr: client.NewGVR("one"),
aliases: []string{"blee", "duh"},
}, {
cmd: "two",
gvr: client.NewGVR("two"),
aliases: []string{"blee", "duh", "fred", "zorg"},
},
},
registeredCommands: map[string]string{
"blee": "one",
"duh": "one",
"fred": "two",
"zorg": "two",
registeredCommands: map[string]*client.GVR{
"blee": client.NewGVR("one"),
"duh": client.NewGVR("one"),
"fred": client.NewGVR("two"),
"zorg": client.NewGVR("two"),
},
},
}
@ -94,7 +95,7 @@ func TestAliasDefine(t *testing.T) {
configAlias := config.NewAliases()
for _, aliases := range u.aliases {
for _, a := range aliases.aliases {
configAlias.Define(aliases.cmd, a)
configAlias.Define(aliases.gvr, a)
}
}
for alias, cmd := range u.registeredCommands {
@ -109,33 +110,39 @@ func TestAliasDefine(t *testing.T) {
func TestAliasesLoad(t *testing.T) {
config.AppConfigDir = "testdata/aliases"
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.Equal(t, 54, len(a.Alias))
assert.Len(t, a.Alias, 55)
}
func TestAliasesSave(t *testing.T) {
assert.NoError(t, data.EnsureFullPath("/tmp/test-aliases", data.DefaultDirMod))
defer assert.NoError(t, os.RemoveAll("/tmp/test-aliases"))
require.NoError(t, data.EnsureFullPath("/tmp/test-aliases", data.DefaultDirMod))
defer require.NoError(t, os.RemoveAll("/tmp/test-aliases"))
config.AppAliasesFile = "/tmp/test-aliases/aliases.yaml"
a := testAliases()
c := len(a.Alias)
assert.Equal(t, c, len(a.Alias))
assert.Nil(t, a.Save())
assert.Nil(t, a.LoadFile(config.AppAliasesFile))
assert.Equal(t, c, len(a.Alias))
assert.Len(t, a.Alias, c)
require.NoError(t, a.Save())
require.NoError(t, a.LoadFile(config.AppAliasesFile))
assert.Len(t, a.Alias, c)
}
// Helpers...
var (
gvr1 = client.NewGVR("gvr1")
gvr2 = client.NewGVR("gvr2")
gvr3 = client.NewGVR("gvr3")
)
func testAliases() *config.Aliases {
a := config.NewAliases()
a.Alias["a1"] = "gvr1"
a.Alias["a11"] = "gvr1"
a.Alias["a2"] = "gvr2"
a.Alias["a3"] = "gvr3"
a.Alias["a1"] = gvr1
a.Alias["a11"] = gvr1
a.Alias["a2"] = gvr2
a.Alias["a3"] = gvr3
return a
}

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import (
"github.com/derailed/k9s/internal/config/data"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSanitizeFileName(t *testing.T) {
@ -65,27 +66,27 @@ func TestHelperInList(t *testing.T) {
func TestEnsureDirPathNone(t *testing.T) {
const mod = 0744
dir := filepath.Join("/tmp", "k9s-test")
dir := filepath.Join(os.TempDir(), "k9s-test")
_ = os.Remove(dir)
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)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, "drwxr--r--", p.Mode().String())
}
func TestEnsureDirPathNoOpt(t *testing.T) {
var mod os.FileMode = 0744
dir := filepath.Join("/tmp", "k9s-test")
assert.NoError(t, os.RemoveAll(dir))
assert.NoError(t, os.Mkdir(dir, mod))
dir := filepath.Join(os.TempDir(), "k9s-test")
require.NoError(t, os.RemoveAll(dir))
require.NoError(t, os.Mkdir(dir, mod))
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)
assert.NoError(t, err)
require.NoError(t, err)
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.
func (n *Namespace) SetActive(ns string, ks KubeSettings) error {
func (n *Namespace) SetActive(ns string, _ KubeSettings) error {
if n == nil {
n = NewActiveNamespace(ns)
}
@ -111,7 +111,7 @@ func (n *Namespace) addFavNS(ns string) {
nfv := make([]string, 0, MaxFavoritesNS)
nfv = append(nfv, ns)
for i := 0; i < len(n.Favorites); i++ {
for i := range n.Favorites {
if i+1 < MaxFavoritesNS {
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/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNSValidate(t *testing.T) {
@ -41,7 +42,7 @@ func TestNsValidateMaxNS(t *testing.T) {
ns.Favorites = allNS
ns.Validate(mock.NewMockConnection())
assert.Equal(t, data.MaxFavoritesNS, len(ns.Favorites))
assert.Len(t, ns.Favorites, data.MaxFavoritesNS)
}
func TestNSSetActive(t *testing.T) {
@ -61,7 +62,7 @@ func TestNSSetActive(t *testing.T) {
ns := data.NewNamespace()
for _, u := range uu {
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.fav, ns.Favorites)
}

View File

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

View File

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

View File

@ -11,21 +11,22 @@ import (
"github.com/adrg/xdg"
"github.com/derailed/k9s/internal/config/data"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_initXDGLocs(t *testing.T) {
tmp, err := UserTmpDir()
assert.NoError(t, err)
require.NoError(t, err)
assert.NoError(t, os.Unsetenv("XDG_CONFIG_HOME"))
assert.NoError(t, os.Unsetenv("XDG_CACHE_HOME"))
assert.NoError(t, os.Unsetenv("XDG_STATE_HOME"))
assert.NoError(t, os.Unsetenv("XDG_DATA_HOME"))
require.NoError(t, os.Unsetenv("XDG_CONFIG_HOME"))
require.NoError(t, os.Unsetenv("XDG_CACHE_HOME"))
require.NoError(t, os.Unsetenv("XDG_STATE_HOME"))
require.NoError(t, os.Unsetenv("XDG_DATA_HOME"))
assert.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")))
assert.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_CONFIG_HOME", filepath.Join(tmp, "k9s-xdg", "config")))
require.NoError(t, os.Setenv("XDG_CACHE_HOME", filepath.Join(tmp, "k9s-xdg", "cache")))
require.NoError(t, os.Setenv("XDG_STATE_HOME", filepath.Join(tmp, "k9s-xdg", "state")))
require.NoError(t, os.Setenv("XDG_DATA_HOME", filepath.Join(tmp, "k9s-xdg", "data")))
xdg.Reload()
uu := map[string]struct {
@ -55,7 +56,7 @@ func Test_initXDGLocs(t *testing.T) {
for k := range uu {
u := uu[k]
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.configFile, AppConfigFile)
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.contextConfig, AppContextConfig("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)
bdir, err := EnsureBenchmarksDir("cl-1", "ct-1-1")
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, u.benchDir, bdir)
hk, err := EnsureHotkeysCfgFile()
assert.NoError(t, err)
require.NoError(t, err)
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/data"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestInitLogLoc(t *testing.T) {
tmp, err := config.UserTmpDir()
assert.NoError(t, err)
require.NoError(t, err)
uu := map[string]struct {
dir string
@ -39,33 +40,33 @@ func TestInitLogLoc(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.NoError(t, os.Unsetenv(config.K9sEnvLogsDir))
assert.NoError(t, os.Unsetenv("XDG_STATE_HOME"))
assert.NoError(t, os.Unsetenv(config.K9sEnvConfigDir))
require.NoError(t, os.Unsetenv(config.K9sEnvLogsDir))
require.NoError(t, os.Unsetenv("XDG_STATE_HOME"))
require.NoError(t, os.Unsetenv(config.K9sEnvConfigDir))
switch k {
case "log-env":
assert.NoError(t, os.Setenv(config.K9sEnvLogsDir, u.dir))
require.NoError(t, os.Setenv(config.K9sEnvLogsDir, u.dir))
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()
case "cfg-env":
assert.NoError(t, os.Setenv(config.K9sEnvConfigDir, u.dir))
require.NoError(t, os.Setenv(config.K9sEnvConfigDir, u.dir))
}
err := config.InitLogLoc()
assert.NoError(t, err)
require.NoError(t, err)
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) {
assert.NoError(t, os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config"))
assert.NoError(t, config.InitLocs())
defer assert.NoError(t, os.RemoveAll("/tmp/test-config"))
require.NoError(t, os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config"))
require.NoError(t, config.InitLocs())
defer require.NoError(t, os.RemoveAll("/tmp/test-config"))
assert.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, data.EnsureFullPath("/tmp/test-config/clusters/cl-1/ct-2", data.DefaultDirMod))
require.NoError(t, os.WriteFile("/tmp/test-config/clusters/cl-1/ct-2/benchmarks.yaml", []byte{}, data.DefaultFileMod))
uu := map[string]struct {
cluster, context string
@ -88,10 +89,10 @@ func TestEnsureBenchmarkCfg(t *testing.T) {
u := uu[k]
t.Run(k, func(t *testing.T) {
f, err := config.EnsureBenchmarksCfgFile(u.cluster, u.context)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, u.f, f)
bb, err := os.ReadFile(f)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, u.e, string(bb))
})
}
@ -99,7 +100,7 @@ func TestEnsureBenchmarkCfg(t *testing.T) {
func TestSkinFileFromName(t *testing.T) {
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 {
n string

View File

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

View File

@ -8,18 +8,18 @@ import (
"github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHotKeyLoad(t *testing.T) {
h := config.NewHotKeys()
assert.NoError(t, h.LoadHotKeys("testdata/hotkeys/hotkeys.yaml"))
assert.Equal(t, 1, len(h.HotKey))
require.NoError(t, h.LoadHotKeys("testdata/hotkeys/hotkeys.yaml"))
assert.Len(t, h.HotKey, 1)
k, ok := h.HotKey["pods"]
assert.True(t, ok)
assert.Equal(t, "shift-0", k.ShortCut)
assert.Equal(t, "Launch pod view", k.Description)
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
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/derailed/k9s/internal/config/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidatePluginSnippet(t *testing.T) {
plugPath := "testdata/plugins/snippet.yaml"
bb, err := os.ReadFile(plugPath)
assert.NoError(t, err)
require.NoError(t, err)
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) {
@ -51,7 +50,7 @@ func TestValidatePlugins(t *testing.T) {
u := uu[k]
t.Run(k, func(t *testing.T) {
bb, err := os.ReadFile(u.path)
assert.NoError(t, err)
require.NoError(t, err)
v := json.NewValidator()
if err := v.Validate(u.schema, bb); err != nil {
assert.Equal(t, u.err, err.Error())
@ -63,7 +62,7 @@ func TestValidatePlugins(t *testing.T) {
func TestValidatePluginDir(t *testing.T) {
plugDir := "../../../plugins"
ee, err := os.ReadDir(plugDir)
assert.NoError(t, err)
require.NoError(t, err)
for _, e := range ee {
if e.IsDir() {
continue
@ -72,31 +71,31 @@ func TestValidatePluginDir(t *testing.T) {
if ext == ".md" {
continue
}
assert.True(t, ext == ".yaml", fmt.Sprintf("expected yaml file: %q", e.Name()))
assert.False(t, strings.Contains(e.Name(), "_"), fmt.Sprintf("underscore in: %q", e.Name()))
assert.Equal(t, ".yaml", ext, "expected yaml file: %q", e.Name())
assert.NotContains(t, "_", e.Name(), "underscore in: %q", e.Name())
bb, err := os.ReadFile(filepath.Join(plugDir, e.Name()))
assert.NoError(t, err)
require.NoError(t, err)
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) {
skinDir := "../../../skins"
ee, err := os.ReadDir(skinDir)
assert.NoError(t, err)
require.NoError(t, err)
p := json.NewValidator()
for _, e := range ee {
if e.IsDir() {
continue
}
ext := filepath.Ext(e.Name())
assert.True(t, ext == ".yaml", fmt.Sprintf("expected yaml file: %q", e.Name()))
assert.True(t, !strings.Contains(e.Name(), "_"), fmt.Sprintf("underscore in: %q", e.Name()))
assert.Equal(t, ".yaml", ext, "expected yaml file: %q", e.Name())
assert.NotContains(t, "_", e.Name(), "underscore in: %q", e.Name())
bb, err := os.ReadFile(filepath.Join(skinDir, e.Name()))
assert.NoError(t, err)
assert.NoError(t, p.Validate(json.SkinSchema, bb), e.Name())
require.NoError(t, err)
require.NoError(t, p.Validate(json.SkinSchema, bb), e.Name())
}
}
@ -119,7 +118,7 @@ func TestValidateSkin(t *testing.T) {
u := uu[k]
t.Run(k, func(t *testing.T) {
bb, err := os.ReadFile(u.f)
assert.NoError(t, err)
require.NoError(t, err)
if err := v.Validate(json.SkinSchema, bb); err != nil {
assert.Equal(t, u.err, err.Error())
}
@ -146,7 +145,7 @@ func TestValidateK9s(t *testing.T) {
u := uu[k]
t.Run(k, func(t *testing.T) {
bb, err := os.ReadFile(u.f)
assert.NoError(t, err)
require.NoError(t, err)
if err := v.Validate(json.K9sSchema, bb); err != nil {
assert.Equal(t, u.err, err.Error())
}
@ -174,7 +173,7 @@ Additional property namespaces is not allowed`,
u := uu[k]
t.Run(k, func(t *testing.T) {
bb, err := os.ReadFile(u.f)
assert.NoError(t, err)
require.NoError(t, err)
if err := v.Validate(json.ContextSchema, bb); err != nil {
assert.Equal(t, u.err, err.Error())
}
@ -202,7 +201,7 @@ aliases is required`,
u := uu[k]
t.Run(k, func(t *testing.T) {
bb, err := os.ReadFile(u.f)
assert.NoError(t, err)
require.NoError(t, err)
if err := v.Validate(json.AliasesSchema, bb); err != nil {
assert.Equal(t, u.err, err.Error())
}
@ -232,7 +231,7 @@ columns is required`,
u := uu[k]
t.Run(k, func(t *testing.T) {
bb, err := os.ReadFile(u.f)
assert.NoError(t, err)
require.NoError(t, err)
if err := v.Validate(json.ViewsSchema, bb); err != nil {
assert.Equal(t, u.err, err.Error())
}

View File

@ -24,14 +24,14 @@ type K9s struct {
LiveViewAutoRefresh bool `json:"liveViewAutoRefresh" yaml:"liveViewAutoRefresh"`
ScreenDumpDir string `json:"screenDumpDir" yaml:"screenDumpDir,omitempty"`
RefreshRate int `json:"refreshRate" yaml:"refreshRate"`
MaxConnRetry int `json:"maxConnRetry" yaml:"maxConnRetry"`
MaxConnRetry int32 `json:"maxConnRetry" yaml:"maxConnRetry"`
ReadOnly bool `json:"readOnly" yaml:"readOnly"`
NoExitOnCtrlC bool `json:"noExitOnCtrlC" yaml:"noExitOnCtrlC"`
PortForwardAddress string `yaml:"portForwardAddress"`
UI UI `json:"ui" yaml:"ui"`
SkipLatestRevCheck bool `json:"skipLatestRevCheck" yaml:"skipLatestRevCheck"`
DisablePodCounting bool `json:"disablePodCounting" yaml:"disablePodCounting"`
ShellPod ShellPod `json:"shellPod" yaml:"shellPod"`
ShellPod *ShellPod `json:"shellPod" yaml:"shellPod"`
ImageScans ImageScans `json:"imageScans" yaml:"imageScans"`
Logger Logger `json:"logger" yaml:"logger"`
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 {
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())
}
@ -298,8 +302,8 @@ func (k *K9s) Override(k9sFlags *Flags) {
k.manualReadOnly = k9sFlags.ReadOnly
}
if k9sFlags.Write != nil && *k9sFlags.Write {
var false bool
k.manualReadOnly = &false
var falseVal bool
k.manualReadOnly = &falseVal
}
k.manualCommand = k9sFlags.Command
k.manualScreenDumpDir = k9sFlags.ScreenDumpDir
@ -382,7 +386,7 @@ func (k *K9s) Validate(c client.Connection, contextName, clusterName string) {
if k.getActiveConfig() == nil {
_, _ = k.ActivateContext(contextName)
}
k.ShellPod = k.ShellPod.Validate()
k.ShellPod.Validate()
k.Logger = k.Logger.Validate()
k.Thresholds = k.Thresholds.Validate()

View File

@ -7,13 +7,14 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_k9sOverrides(t *testing.T) {
var (
true = true
cmd = "po"
dir = "/tmp/blee"
trueVal = true
cmd = "po"
dir = "/tmp/blee"
)
uu := map[string]struct {
@ -71,15 +72,15 @@ func Test_k9sOverrides(t *testing.T) {
Headless: false,
Logoless: false,
Crumbsless: false,
manualHeadless: &true,
manualLogoless: &true,
manualCrumbsless: &true,
manualSplashless: &true,
manualHeadless: &trueVal,
manualLogoless: &trueVal,
manualCrumbsless: &trueVal,
manualSplashless: &trueVal,
},
SkipLatestRevCheck: false,
DisablePodCounting: false,
manualRefreshRate: 100,
manualReadOnly: &true,
manualReadOnly: &trueVal,
manualCommand: &cmd,
manualScreenDumpDir: &dir,
},
@ -123,7 +124,7 @@ func Test_screenDumpDirOverride(t *testing.T) {
u := uu[k]
t.Run(k, func(t *testing.T) {
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
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/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
@ -94,7 +95,7 @@ func TestK9sMerge(t *testing.T) {
UI: config.UI{},
SkipLatestRevCheck: false,
DisablePodCounting: false,
ShellPod: config.ShellPod{},
ShellPod: new(config.ShellPod),
ImageScans: config.ImageScans{},
Logger: config.Logger{},
Thresholds: nil,
@ -135,14 +136,14 @@ func TestContextScreenDumpDir(t *testing.T) {
cfg := mock.NewMockConfig()
_, err := cfg.K9s.ActivateContext("ct-1-1")
assert.NoError(t, err)
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true))
require.NoError(t, err)
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())
}
func TestAppScreenDumpDir(t *testing.T) {
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())
}

View File

@ -89,7 +89,7 @@ func (m mockKubeSettings) CurrentContextName() (string, error) {
func (m mockKubeSettings) CurrentClusterName() (string, error) {
return *m.flags.ClusterName, nil
}
func (m mockKubeSettings) CurrentNamespaceName() (string, error) {
func (mockKubeSettings) CurrentNamespaceName() (string, error) {
return "default", nil
}
func (m mockKubeSettings) GetContext(s string) (*api.Context, error) {
@ -111,7 +111,7 @@ func (m mockKubeSettings) ContextNames() (map[string]struct{}, error) {
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 {
ct string
@ -124,57 +124,57 @@ func NewMockConnectionWithContext(ct string) mockConnection {
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
}
func (m mockConnection) Config() *client.Config {
func (mockConnection) Config() *client.Config {
return nil
}
func (m mockConnection) ConnectionOK() bool {
func (mockConnection) ConnectionOK() bool {
return false
}
func (m mockConnection) Dial() (kubernetes.Interface, error) {
func (mockConnection) Dial() (kubernetes.Interface, error) {
return nil, nil
}
func (m mockConnection) DialLogs() (kubernetes.Interface, error) {
func (mockConnection) DialLogs() (kubernetes.Interface, error) {
return nil, nil
}
func (m mockConnection) SwitchContext(ctx string) error {
func (mockConnection) SwitchContext(string) error {
return nil
}
func (m mockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) {
func (mockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) {
return nil, nil
}
func (m mockConnection) RestConfig() (*restclient.Config, error) {
func (mockConnection) RestConfig() (*restclient.Config, error) {
return nil, nil
}
func (m mockConnection) MXDial() (*versioned.Clientset, error) {
func (mockConnection) MXDial() (*versioned.Clientset, error) {
return nil, nil
}
func (m mockConnection) DynDial() (dynamic.Interface, error) {
func (mockConnection) DynDial() (dynamic.Interface, error) {
return nil, nil
}
func (m mockConnection) HasMetrics() bool {
func (mockConnection) HasMetrics() bool {
return false
}
func (m mockConnection) ValidNamespaceNames() (client.NamespaceNames, error) {
func (mockConnection) ValidNamespaceNames() (client.NamespaceNames, error) {
return nil, nil
}
func (m mockConnection) IsValidNamespace(string) bool {
func (mockConnection) IsValidNamespace(string) bool {
return true
}
func (m mockConnection) ServerVersion() (*version.Info, error) {
func (mockConnection) ServerVersion() (*version.Info, error) {
return nil, nil
}
func (m mockConnection) CheckConnectivity() bool {
func (mockConnection) CheckConnectivity() bool {
return false
}
func (m mockConnection) ActiveContext() string {
return m.ct
}
func (m mockConnection) ActiveNamespace() string {
func (mockConnection) ActiveNamespace() string {
return ""
}
func (m mockConnection) IsActiveNamespace(string) bool {
func (mockConnection) IsActiveNamespace(string) bool {
return false
}

View File

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

View File

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

View File

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

View File

@ -20,14 +20,21 @@ type StyleListener interface {
StylesChanged(*Styles)
}
// TextStyle tracks text styles.
type TextStyle string
const (
// TextStyleNormal is the default text style.
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 {
switch ts {
case TextStyleNormal:
@ -283,8 +290,8 @@ func newCharts() Charts {
DefaultDialColors: Colors{Color("palegreen"), Color("orangered")},
DefaultChartColors: Colors{Color("palegreen"), Color("orangered")},
ResourceColors: map[string]Colors{
"cpu": {Color("dodgerblue"), Color("darkslateblue")},
"mem": {Color("yellow"), Color("goldenrod")},
CPU: {Color("dodgerblue"), Color("darkslateblue")},
MEM: {Color("yellow"), Color("goldenrod")},
},
}
}

View File

@ -10,6 +10,7 @@ import (
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewStyle(t *testing.T) {
@ -38,7 +39,7 @@ func TestColor(t *testing.T) {
func TestSkinHappy(t *testing.T) {
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()
assert.Equal(t, "#ffffff", s.Body().FgColor.String())

View File

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

View File

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

View File

@ -6,6 +6,12 @@ package config
const (
defaultRefreshRate = 2
defaultMaxConnRetry = 5
// CPU tracks cpu usage.
CPU = "cpu"
// MEM tracks memory usage.
MEM = "memory"
)
// 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 == "")
}
func (v *ViewSetting) SortCol() (string, bool, error) {
func (v *ViewSetting) SortCol() (name string, asc bool, err error) {
if v == nil || v.SortColumn == "" {
return "", false, fmt.Errorf("no sort column specified")
}
@ -180,9 +180,7 @@ func (v *CustomView) getVS(gvr, ns string) *ViewSetting {
}
k := gvr
kk := slices.Collect(maps.Keys(v.Views))
slices.SortFunc(kk, func(s1, s2 string) int {
return strings.Compare(s1, s2)
})
slices.SortFunc(kk, strings.Compare)
slices.Reverse(kk)
for _, key := range kk {
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]
return &vs
}
}
return nil

View File

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

View File

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

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/config"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/view/cmd"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
)
var _ Accessor = (*Alias)(nil)
@ -32,22 +32,18 @@ func NewAlias(f Factory) *Alias {
a := Alias{
Aliases: config.NewAliases(),
}
a.Init(f, client.NewGVR("aliases"))
a.Init(f, client.AliGVR)
return &a
}
func (a *Alias) AliasesFor(s string) []string {
return a.Aliases.AliasesFor(s)
}
// Check verifies an alias is defined for this command.
func (a *Alias) Check(cmd string) (string, bool) {
return a.Aliases.Get(cmd)
// AliasesFor returns a set of aliases for a given gvr.
func (a *Alias) AliasesFor(gvr *client.GVR) sets.Set[string] {
return a.Aliases.AliasesFor(gvr)
}
// 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)
if !ok {
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.
func (a *Alias) AsGVR(c string) (client.GVR, string, bool) {
exp, ok := a.Aliases.Get(c)
if !ok {
return client.NoGVR, "", 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
func (a *Alias) AsGVR(alias string) (*client.GVR, string, bool) {
gvr, ok := a.Aliases.Get(alias)
if ok {
if pgvr := MetaAccess.Lookup(alias); pgvr != client.NoGVR {
return pgvr, "", ok
}
}
return client.NoGVR, "", false
return gvr, "", ok
}
// 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")
}
@ -109,25 +100,21 @@ func (a *Alias) load(path string) error {
if IsK9sMeta(meta) {
continue
}
gvrStr := gvr.String()
if IsCRD(meta) {
crdGVRS = append(crdGVRS, gvr)
continue
}
a.Define(gvrStr, gvr.AsResourceName())
a.Define(gvr, gvr.AsResourceName())
// Allow single shot commands for k8s resources only!
if isStandardGroup(gvr.GVSub()) {
a.Define(gvrStr, strings.ToLower(meta.Kind), meta.Name)
a.Define(gvrStr, meta.SingularName)
a.Define(gvr, meta.Name)
a.Define(gvr, meta.SingularName)
}
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 {
@ -135,15 +122,14 @@ func (a *Alias) load(path string) error {
if err != nil {
return err
}
gvrStr := gvr.String()
a.Define(gvrStr, strings.ToLower(meta.Kind), meta.Name)
a.Define(gvrStr, meta.SingularName)
a.Define(gvr, strings.ToLower(meta.Kind), meta.Name)
a.Define(gvr, meta.SingularName)
if len(meta.ShortNames) > 0 {
a.Define(gvrStr, meta.ShortNames...)
a.Define(gvr, meta.ShortNames...)
}
a.Define(gvrStr, gvrStr)
a.Define(gvrStr, meta.Name+"."+meta.Group)
a.Define(gvr, gvr.String())
a.Define(gvr, meta.Name+"."+meta.Group)
}
return nil

View File

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

View File

@ -30,17 +30,17 @@ type Benchmark struct {
}
// 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)
}
// 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")
}
// 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)
if !ok {
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/render"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBenchmarkList(t *testing.T) {
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(ctx, internal.KeyPath, "")
oo, err := a.List(ctx, "-")
assert.Nil(t, err)
assert.Equal(t, 1, len(oo))
require.NoError(t, err)
assert.Len(t, oo, 1)
assert.Equal(t, "testdata/bench/default_fred_1577308050814961000.txt", oo[0].(render.BenchInfo).Path)
}

View File

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

View File

@ -54,14 +54,33 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
return nil, err
}
res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers)+len(po.Spec.EphemeralContainers))
for i, co := range po.Spec.InitContainers {
res = append(res, makeContainerRes(initIDX, i, co, po, cmx[co.Name]))
for i := range po.Spec.InitContainers {
res = append(res, makeContainerRes(
initIDX,
i,
&(po.Spec.InitContainers[i]),
po,
cmx[po.Spec.InitContainers[i].Name]),
)
}
for i, co := range po.Spec.Containers {
res = append(res, makeContainerRes(mainIDX, i, co, po, cmx[co.Name]))
for i := range po.Spec.Containers {
res = append(res, makeContainerRes(
mainIDX,
i,
&(po.Spec.Containers[i]),
po,
cmx[po.Spec.Containers[i].Name]),
)
}
for i, co := range po.Spec.EphemeralContainers {
res = append(res, makeContainerRes(ephIDX, i, v1.Container(co.EphemeralContainerCommon), po, cmx[co.Name]))
for i := range po.Spec.EphemeralContainers {
co := v1.Container(po.Spec.EphemeralContainers[i].EphemeralContainerCommon)
res = append(res, makeContainerRes(
ephIDX,
i,
&co,
po,
cmx[co.Name]),
)
}
return res, nil
@ -70,7 +89,7 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
// TailLogs tails a given container logs.
func (c *Container) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {
po := Pod{}
po.Init(c.Factory, client.NewGVR("v1/pods"))
po.Init(c.Factory, client.PodGVR)
return po.TailLogs(ctx, opts)
}
@ -78,34 +97,34 @@ func (c *Container) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan,
// ----------------------------------------------------------------------------
// 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{
Idx: kind + strconv.Itoa(idx+1),
Container: &co,
Status: getContainerStatus(kind, co.Name, po.Status),
Container: co,
Status: getContainerStatus(kind, co.Name, &po.Status),
MX: cmx,
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 {
case mainIDX:
for _, s := range status.ContainerStatuses {
if s.Name == name {
return &s
for i := range status.ContainerStatuses {
if status.ContainerStatuses[i].Name == name {
return &status.ContainerStatuses[i]
}
}
case initIDX:
for _, s := range status.InitContainerStatuses {
if s.Name == name {
return &s
for i := range status.InitContainerStatuses {
if status.InitContainerStatuses[i].Name == name {
return &status.InitContainerStatuses[i]
}
}
case ephIDX:
for _, s := range status.EphemeralContainerStatuses {
if s.Name == name {
return &s
for i := range status.EphemeralContainerStatuses {
if status.EphemeralContainerStatuses[i].Name == name {
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) {
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 {
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/watch"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
@ -28,12 +29,12 @@ import (
func TestContainerList(t *testing.T) {
c := dao.Container{}
c.Init(makePodFactory(), client.NewGVR("containers"))
c.Init(makePodFactory(), client.CoGVR)
ctx := context.WithValue(context.Background(), internal.KeyPath, "fred/p1")
oo, err := c.List(ctx, "")
assert.Nil(t, err)
assert.Equal(t, 1, len(oo))
require.NoError(t, err)
assert.Len(t, oo, 1)
}
// ----------------------------------------------------------------------------
@ -45,58 +46,58 @@ func makeConn() *conn {
return &conn{}
}
func (c *conn) Config() *client.Config { return nil }
func (c *conn) Dial() (kubernetes.Interface, error) { return nil, nil }
func (c *conn) DialLogs() (kubernetes.Interface, error) { return nil, nil }
func (c *conn) ConnectionOK() bool { return true }
func (c *conn) SwitchContext(ctx string) error { return nil }
func (c *conn) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { return nil, nil }
func (c *conn) RestConfig() (*restclient.Config, error) { return nil, nil }
func (c *conn) MXDial() (*versioned.Clientset, error) { return nil, nil }
func (c *conn) DynDial() (dynamic.Interface, error) { return nil, nil }
func (c *conn) HasMetrics() bool { return false }
func (c *conn) CheckConnectivity() bool { return false }
func (c *conn) IsNamespaced(n string) bool { return false }
func (c *conn) SupportsResource(group string) bool { return false }
func (c *conn) ValidNamespaces() ([]v1.Namespace, error) { return nil, nil }
func (c *conn) SupportsRes(grp string, versions []string) (string, bool, error) {
return "", false, nil
}
func (c *conn) ServerVersion() (*version.Info, error) { return nil, nil }
func (c *conn) CurrentNamespaceName() (string, error) { return "", nil }
func (c *conn) CanI(ns, gvr, n string, verbs []string) (bool, error) { return true, nil }
func (c *conn) ActiveContext() string { return "" }
func (c *conn) ActiveNamespace() string { return "" }
func (c *conn) IsValidNamespace(string) bool { return true }
func (c *conn) ValidNamespaceNames() (client.NamespaceNames, error) { return nil, nil }
func (c *conn) IsActiveNamespace(string) bool { return false }
func (*conn) Config() *client.Config { return nil }
func (*conn) Dial() (kubernetes.Interface, error) { return nil, nil }
func (*conn) DialLogs() (kubernetes.Interface, error) { return nil, nil }
func (*conn) ConnectionOK() bool { return true }
func (*conn) SwitchContext(string) error { return nil }
func (*conn) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { return nil, nil }
func (*conn) RestConfig() (*restclient.Config, error) { return nil, nil }
func (*conn) MXDial() (*versioned.Clientset, error) { return nil, nil }
func (*conn) DynDial() (dynamic.Interface, error) { return nil, nil }
func (*conn) HasMetrics() bool { return false }
func (*conn) CheckConnectivity() bool { return false }
func (*conn) IsNamespaced(string) bool { return false }
func (*conn) SupportsResource(string) bool { return false }
func (*conn) ValidNamespaces() ([]v1.Namespace, error) { return nil, nil }
func (*conn) SupportsRes(string, []string) (a string, b bool, e error) { return "", false, nil }
func (*conn) ServerVersion() (*version.Info, error) { return nil, nil }
func (*conn) CurrentNamespaceName() (string, error) { return "", nil }
func (*conn) CanI(string, *client.GVR, string, []string) (bool, error) { return true, nil }
func (*conn) ActiveContext() string { return "" }
func (*conn) ActiveNamespace() string { return "" }
func (*conn) IsValidNamespace(string) bool { return true }
func (*conn) ValidNamespaceNames() (client.NamespaceNames, error) { return nil, nil }
func (*conn) IsActiveNamespace(string) bool { return false }
type podFactory struct{}
var _ dao.Factory = &testFactory{}
func (f podFactory) Client() client.Connection {
func (podFactory) Client() client.Connection {
return makeConn()
}
func (f podFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
var m map[string]interface{}
func (podFactory) Get(*client.GVR, string, bool, labels.Selector) (runtime.Object, error) {
var m map[string]any
if err := yaml.Unmarshal([]byte(poYaml()), &m); err != nil {
return nil, err
}
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
}
func (f podFactory) ForResource(ns, gvr string) (informers.GenericInformer, error) { return nil, nil }
func (f podFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) {
func (podFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) {
return nil, nil
}
func (f podFactory) WaitForCacheSync() {}
func (f podFactory) Forwarders() watch.Forwarders { return nil }
func (f podFactory) DeleteForwarder(string) {}
func (podFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) {
return nil, nil
}
func (podFactory) WaitForCacheSync() {}
func (podFactory) Forwarders() watch.Forwarders { return nil }
func (podFactory) DeleteForwarder(string) {}
func makePodFactory() dao.Factory {
return podFactory{}

View File

@ -28,7 +28,7 @@ func (c *Context) config() *client.Config {
}
// 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)
if err != nil {
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.
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()
if err != nil {
return nil, err

View File

@ -20,10 +20,7 @@ import (
"k8s.io/apimachinery/pkg/util/rand"
)
const (
maxJobNameSize = 42
jobGVR = "batch/v1/jobs"
)
const maxJobNameSize = 42
var (
_ Accessor = (*CronJob)(nil)
@ -37,7 +34,7 @@ type CronJob struct {
}
// 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)
if err != nil {
return nil, err
@ -49,7 +46,7 @@ func (c *CronJob) ListImages(ctx context.Context, fqn string) ([]string, error)
// Run a CronJob.
func (c *CronJob) Run(path string) error {
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 {
return err
}
@ -57,7 +54,7 @@ func (c *CronJob) Run(path string) error {
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 {
return err
}
@ -70,7 +67,7 @@ func (c *CronJob) Run(path string) error {
if len(cj.Name) >= maxJobNameSize {
jobName = cj.Name[0:maxJobNameSize]
}
true := true
trueVal := true
job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: jobName + "-manual-" + rand.String(3),
@ -81,8 +78,8 @@ func (c *CronJob) Run(path string) error {
{
APIVersion: c.gvr.GV().String(),
Kind: "CronJob",
BlockOwnerDeletion: &true,
Controller: &true,
BlockOwnerDeletion: &trueVal,
Controller: &trueVal,
Name: cj.Name,
UID: cj.UID,
},
@ -102,9 +99,9 @@ func (c *CronJob) Run(path string) error {
}
// 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)
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 {
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.
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 {
return nil, err
}
@ -146,7 +143,7 @@ func (c *CronJob) GetInstance(fqn string) (*batchv1.CronJob, error) {
// ToggleSuspend toggles suspend/resume on a CronJob.
func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error {
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 {
return err
}
@ -166,8 +163,8 @@ func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error {
current := !*cj.Spec.Suspend
cj.Spec.Suspend = &current
} else {
true := true
cj.Spec.Suspend = &true
trueVal := true
cj.Spec.Suspend = &trueVal
}
_, 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.
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)
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 {
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")
}
switch gvr {
case CmGVR:
case client.CmGVR:
if !hasConfigMap(&cj.Spec.JobTemplate.Spec.Template.Spec, n) {
continue
}
@ -198,7 +195,7 @@ func (c *CronJob) Scan(ctx context.Context, gvr client.GVR, fqn string, wait boo
GVR: c.GVR(),
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)
if err != nil {
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(),
FQN: client.FQN(cj.Namespace, cj.Name),
})
case PcGVR:
case client.PcGVR:
if !hasPC(&cj.Spec.JobTemplate.Spec.Template.Spec, n) {
continue
}

View File

@ -10,12 +10,12 @@ import (
"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)
if !ok {
panic("no unstructured")
}
m, ok := u.Object[field].(map[string]interface{})
m, ok := u.Object[field].(map[string]any)
if !ok {
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
}
func mustSlice(o runtime.Object, field string) []interface{} {
func mustSlice(o runtime.Object, field string) []any {
u, ok := o.(*unstructured.Unstructured)
if !ok {
return []interface{}{}
return nil
}
s, ok := u.Object[field].([]interface{})
s, ok := u.Object[field].([]any)
if !ok {
return []interface{}{}
return nil
}
return s
}
func mustField(o map[string]interface{}, field string) interface{} {
func mustField(o map[string]any, field string) any {
f, ok := o[field]
if !ok {
panic(fmt.Sprintf("no field for %q", field))

View File

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

View File

@ -12,7 +12,7 @@ import (
)
// 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}
m, err := mapper.ToRESTMapper()
if err != nil {

View File

@ -27,14 +27,14 @@ type Dir struct {
// NewDir returns a new set of aliases.
func NewDir(f Factory) *Dir {
var a Dir
a.Init(f, client.NewGVR("dir"))
a.Init(f, client.DirGVR)
return &a
}
var yamlRX = regexp.MustCompile(`.*\.(yml|yaml|json)`)
// 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)
if !ok {
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.
func (a *Dir) Get(_ context.Context, _ string) (runtime.Object, error) {
func (*Dir) Get(_ context.Context, _ string) (runtime.Object, error) {
return nil, errors.New("nyi")
}

View File

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

View File

@ -41,7 +41,7 @@ type Deployment struct {
}
// 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)
if err != nil {
return nil, err
@ -52,76 +52,12 @@ func (d *Deployment) ListImages(ctx context.Context, fqn string) ([]string, erro
// Scale a Deployment.
func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error {
ns, n := client.Namespaced(path)
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
return scaleRes(ctx, d.getFactory(), client.DpGVR, path, replicas)
}
// Restart a Deployment rollout.
func (d *Deployment) Restart(ctx context.Context, path string) error {
o, err := d.getFactory().Get("apps/v1/deployments", path, true, labels.Everything())
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
return restartRes[*appsv1.Deployment](ctx, d.getFactory(), client.DpGVR, path)
}
// 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.
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 {
return nil, err
}
@ -164,9 +100,9 @@ func (d *Deployment) GetInstance(fqn string) (*appsv1.Deployment, error) {
}
// 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)
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 {
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.
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)
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 {
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")
}
switch gvr {
case CmGVR:
case client.CmGVR:
if !hasConfigMap(&dp.Spec.Template.Spec, n) {
continue
}
@ -213,7 +149,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr client.GVR, fqn string, wait
GVR: d.GVR(),
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)
if err != nil {
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(),
FQN: client.FQN(dp.Namespace, dp.Name),
})
case PvcGVR:
case client.PvcGVR:
if !hasPVC(&dp.Spec.Template.Spec, n) {
continue
}
@ -237,7 +173,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr client.GVR, fqn string, wait
GVR: d.GVR(),
FQN: client.FQN(dp.Namespace, dp.Name),
})
case PcGVR:
case client.PcGVR:
if !hasPC(&dp.Spec.Template.Spec, n) {
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),
})
}
}
return refs, nil
@ -265,7 +200,7 @@ func (d *Deployment) GetPodSpec(path string) (*v1.PodSpec, error) {
// SetImages sets container images.
func (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
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 {
return err
}
@ -290,9 +225,11 @@ func (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs Imag
return err
}
// Helpers...
func hasPVC(spec *v1.PodSpec, name string) bool {
for _, v := range spec.Volumes {
if v.PersistentVolumeClaim != nil && v.PersistentVolumeClaim.ClaimName == name {
for i := range spec.Volumes {
if spec.Volumes[i].PersistentVolumeClaim != nil && spec.Volumes[i].PersistentVolumeClaim.ClaimName == name {
return true
}
}
@ -304,24 +241,24 @@ func hasPC(spec *v1.PodSpec, name string) bool {
}
func hasConfigMap(spec *v1.PodSpec, name string) bool {
for _, c := range spec.InitContainers {
if containerHasConfigMap(c.EnvFrom, c.Env, name) {
for i := range spec.InitContainers {
if containerHasConfigMap(spec.InitContainers[i].EnvFrom, spec.InitContainers[i].Env, name) {
return true
}
}
for _, c := range spec.Containers {
if containerHasConfigMap(c.EnvFrom, c.Env, name) {
for i := range spec.Containers {
if containerHasConfigMap(spec.Containers[i].EnvFrom, spec.Containers[i].Env, name) {
return true
}
}
for _, c := range spec.EphemeralContainers {
if containerHasConfigMap(c.EnvFrom, c.Env, name) {
for i := range spec.EphemeralContainers {
if containerHasConfigMap(spec.EphemeralContainers[i].EnvFrom, spec.EphemeralContainers[i].Env, name) {
return true
}
}
for _, v := range spec.Volumes {
if cm := v.ConfigMap; cm != nil {
for i := range spec.Volumes {
if cm := spec.Volumes[i].ConfigMap; cm != nil {
if cm.Name == name {
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) {
for _, c := range spec.InitContainers {
if containerHasSecret(c.EnvFrom, c.Env, name) {
for i := range spec.InitContainers {
if containerHasSecret(spec.InitContainers[i].EnvFrom, spec.InitContainers[i].Env, name) {
return true, nil
}
}
for _, c := range spec.Containers {
if containerHasSecret(c.EnvFrom, c.Env, name) {
for i := range spec.Containers {
if containerHasSecret(spec.Containers[i].EnvFrom, spec.Containers[i].Env, name) {
return true, nil
}
}
for _, c := range spec.EphemeralContainers {
if containerHasSecret(c.EnvFrom, c.Env, name) {
for i := range spec.EphemeralContainers {
if containerHasSecret(spec.EphemeralContainers[i].EnvFrom, spec.EphemeralContainers[i].Env, name) {
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 != "" {
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 {
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 {
if sec := v.Secret; sec != nil {
for i := range spec.Volumes {
if sec := spec.Volumes[i].Secret; sec != nil {
if sec.SecretName == name {
return true, nil
}
}
}
return false, nil
}
@ -419,3 +357,110 @@ func containerHasConfigMap(envFrom []v1.EnvFromSource, env []v1.EnvVar, name str
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/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/kubectl/pkg/polymorphichelpers"
"k8s.io/kubectl/pkg/scheme"
)
var (
@ -43,7 +40,7 @@ type DaemonSet struct {
}
// 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)
if err != nil {
return nil, err
@ -54,51 +51,7 @@ func (d *DaemonSet) ListImages(ctx context.Context, fqn string) ([]string, error
// Restart a DaemonSet rollout.
func (d *DaemonSet) Restart(ctx context.Context, path string) error {
o, err := d.getFactory().Get("apps/v1/daemonsets", path, true, labels.Everything())
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
return restartRes[*appsv1.DaemonSet](ctx, d.getFactory(), client.DsGVR, path)
}
// 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)
oo, err := f.List("v1/pods", ns, true, lsel)
oo, err := f.List(client.PodGVR, ns, true, lsel)
if err != nil {
return nil, err
}
opts.MultiPods = true
var po Pod
po.Init(f, client.NewGVR("v1/pods"))
po.Init(f, client.PodGVR)
outs := make([]LogChan, 0, len(oo))
for _, o := range oo {
@ -169,7 +122,7 @@ func (d *DaemonSet) Pod(fqn string) (string, error) {
// GetInstance returns a daemonset instance.
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 {
return nil, err
}
@ -184,9 +137,9 @@ func (d *DaemonSet) GetInstance(fqn string) (*appsv1.DaemonSet, error) {
}
// 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)
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 {
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.
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)
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 {
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")
}
switch gvr {
case CmGVR:
case client.CmGVR:
if !hasConfigMap(&ds.Spec.Template.Spec, n) {
continue
}
@ -233,7 +186,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait b
GVR: d.GVR(),
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)
if err != nil {
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(),
FQN: client.FQN(ds.Namespace, ds.Name),
})
case PvcGVR:
case client.PvcGVR:
if !hasPVC(&ds.Spec.Template.Spec, n) {
continue
}
@ -257,7 +210,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait b
GVR: d.GVR(),
FQN: client.FQN(ds.Namespace, ds.Name),
})
case PcGVR:
case client.PcGVR:
if !hasPC(&ds.Spec.Template.Spec, n) {
continue
}
@ -284,7 +237,7 @@ func (d *DaemonSet) GetPodSpec(path string) (*v1.PodSpec, error) {
// SetImages sets container images.
func (d *DaemonSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
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 {
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) {
strLabel, _ := ctx.Value(internal.KeyLabels).(string)
opts := []string{d.gvr.AsResourceName()}
ns, n := client.Namespaced(fqn)
if n != "" {
opts = append(opts, n)
}
allNS := client.IsAllNamespaces(ns)
flags := cmdutil.NewMatchVersionFlags(d.getFactory().Client().Config().Flags())
f := cmdutil.NewFactory(flags)
@ -70,7 +68,6 @@ func (d *Dynamic) toTable(ctx context.Context, fqn string) ([]runtime.Object, er
if err != nil {
return nil, err
}
oo := make([]runtime.Object, 0, len(infos))
for _, info := range infos {
o, err := decodeIntoTable(info.Object, allNS)
@ -93,7 +90,6 @@ func decodeIntoTable(obj runtime.Object, allNs bool) (runtime.Object, error) {
if isEvent {
obj = event.Object.Object
}
if !recognizedTableVersions[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()
}
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, row.Cells...)
row.Cells = cells

View File

@ -104,7 +104,7 @@ func (g *Generic) ToYAML(path string, showManaged bool) (string, error) {
// Delete deletes a resource.
func (g *Generic) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error {
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 {
return err
}

View File

@ -32,7 +32,7 @@ type HelmChart struct {
}
// 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)
if err != nil {
return nil, err
@ -102,7 +102,7 @@ func (h *HelmChart) Describe(path string) (string, error) {
}
// 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)
cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns)
if err != nil {
@ -164,7 +164,7 @@ func ensureHelmConfig(flags *genericclioptions.ConfigFlags, ns string) (*action.
return cfg, err
}
func helmLogger(fmat string, args ...interface{}) {
func helmLogger(fmat string, args ...any) {
slog.Debug("Log",
slogs.Log, fmt.Sprintf(fmat, args...),
slogs.Subsys, "helm",

View File

@ -59,7 +59,7 @@ func (h *HelmHistory) List(ctx context.Context, _ string) ([]runtime.Object, err
// Get returns a resource.
func (h *HelmHistory) Get(_ context.Context, path string) (runtime.Object, error) {
fqn, rev, found := strings.Cut(path, ":")
if !found || len(rev) == 0 {
if !found || rev == "" {
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.
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)
if err != nil {
return "", err

View File

@ -27,14 +27,14 @@ const (
)
// 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]
if !ok {
return "", false
}
for _, container := range spec.Containers {
if container.Name == defaultContainer {
for i := range spec.Containers {
if spec.Containers[i].Name == defaultContainer {
return defaultContainer, true
}
}
@ -93,7 +93,7 @@ func ToYAML(o runtime.Object, showManaged bool) (string, error) {
if !showManaged {
o = o.DeepCopyObject()
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")
}
}

View File

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

View File

@ -21,7 +21,7 @@ type ImageScan struct {
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)
if err != nil {
return nil, err
@ -40,7 +40,7 @@ func (is *ImageScan) List(ctx context.Context, _ string) ([]runtime.Object, erro
if !ok {
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 {
return nil, fmt.Errorf("no context gvr for %q", is.gvr)
}

View File

@ -32,7 +32,7 @@ type Job struct {
}
// 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)
if err != nil {
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.
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 {
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) {
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 {
return nil, err
}
@ -108,9 +108,9 @@ func (j *Job) GetInstance(fqn string) (*batchv1.Job, error) {
}
// 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)
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 {
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.
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)
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 {
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")
}
switch gvr {
case CmGVR:
case client.CmGVR:
if !hasConfigMap(&job.Spec.Template.Spec, n) {
continue
}
@ -157,7 +157,7 @@ func (j *Job) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (
GVR: j.GVR(),
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)
if err != nil {
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(),
FQN: client.FQN(job.Namespace, job.Name),
})
case PcGVR:
case client.PcGVR:
if !hasPC(&job.Spec.Template.Spec, n) {
continue
}

View File

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

View File

@ -112,7 +112,7 @@ func BenchmarkLogItemRenderTS(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
for range b.N {
bb := bytes.NewBuffer(make([]byte, 0, i.Size()))
i.Render("yellow", true, bb)
}
@ -125,7 +125,7 @@ func BenchmarkLogItemRenderNoTS(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
for range b.N {
bb := bytes.NewBuffer(make([]byte, 0, i.Size()))
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.
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 == "" {
return nil, nil, nil
return
}
if f, ok := internal.IsFuzzySelector(q); ok {
mm, ii := l.fuzzyFilter(index, f, showTime)
return mm, ii, nil
matches, indices = l.fuzzyFilter(index, f, showTime)
return
}
matches, indices, err := l.filterLogs(index, q, showTime)
matches, indices, err = l.filterLogs(index, q, showTime)
if err != nil {
return nil, nil, err
return
}
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)
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))
for _, m := range mm {
matches = append(matches, m.Index)
@ -199,7 +199,7 @@ func (l *LogItems) fuzzyFilter(index int, q string, showTime bool) ([]int, [][]i
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
if internal.IsInverseSelector(q) {
invert = true
@ -209,7 +209,7 @@ func (l *LogItems) filterLogs(index int, q string, showTime bool) ([]int, [][]in
if err != nil {
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:]))
l.Lines(index, showTime, ll)
for i, line := range ll {

View File

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

View File

@ -102,8 +102,8 @@ func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error {
}
if !cordoned {
if err = n.ToggleCordon(path, true); err != nil {
return err
if e := n.ToggleCordon(path, true); e != nil {
return e
}
}
@ -168,7 +168,13 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
}
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))
for _, o := range oo {
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)
podCount := -1
if shouldCountPods {
podCount, err = n.CountPods(name)
podCount, err = n.CountPods(pods, name)
if err != nil {
slog.Error("Unable to get pods count",
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.
func (n *Node) CountPods(nodeName string) (int, error) {
func (*Node) CountPods(oo []runtime.Object, nodeName string) (int, error) {
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 {
u, ok := o.(*unstructured.Unstructured)
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 {
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 {
count++
@ -225,7 +226,7 @@ func (n *Node) CountPods(nodeName string) (int, error) {
// GetPods returns all pods running on given node.
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 {
return nil, err
}
@ -258,9 +259,9 @@ func (n *Node) ensureCordoned(path string) (bool, error) {
// Helpers...
// 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)
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 {
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")
}
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 {
return nil, err
}
@ -283,8 +284,8 @@ func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) {
}
// FetchNodes retrieves all nodes.
func FetchNodes(ctx context.Context, f Factory, labelsSel string) (*v1.NodeList, error) {
auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", "", client.ListAccess)
func FetchNodes(_ context.Context, f Factory, _ string) (*v1.NodeList, error) {
auth, err := f.Client().CanI(client.ClusterScope, client.NodeGVR, "", client.ListAccess)
if err != nil {
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")
}
oo, err := f.List("v1/nodes", "", false, labels.Everything())
oo, err := f.List(client.NodeGVR, "", false, labels.Everything())
if err != nil {
return nil, err
}

View File

@ -16,20 +16,19 @@ import (
type NonResource struct {
Factory
gvr client.GVR
gvr *client.GVR
mx sync.RWMutex
includeObj bool
}
// 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.Factory, n.gvr = f, gvr
}
n.Factory, n.gvr = f, gvr
n.mx.Unlock()
}
// SetIncludeObject sets if resource object should be included in the api server response.
func (n *NonResource) SetIncludeObject(f bool) {
n.includeObj = f
}
@ -57,6 +56,6 @@ func (n *NonResource) GVR() string {
}
// 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")
}

View File

@ -75,7 +75,7 @@ func getPatchPodSpec(imageSpecs ImageSpecs) 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 {
if spec.Init {
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.
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)
if err != nil {
return nil, err
@ -108,7 +108,7 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
continue
}
spec, ok := u.Object["spec"].(map[string]interface{})
spec, ok := u.Object["spec"].(map[string]any)
if !ok {
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.
func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {
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 {
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))
for _, c := range pod.Spec.Containers {
cc = append(cc, c.Name)
for i := range pod.Spec.Containers {
cc = append(cc, pod.Spec.Containers[i].Name)
}
if includeInit {
for _, c := range pod.Spec.InitContainers {
cc = append(cc, c.Name)
for i := range pod.Spec.InitContainers {
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.
func (p *Pod) Pod(fqn string) (string, error) {
func (*Pod) Pod(fqn string) (string, error) {
return fqn, nil
}
// GetInstance returns a pod instance.
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 {
return nil, err
}
@ -187,7 +187,7 @@ func (p *Pod) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error)
if !ok {
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 {
return nil, err
}
@ -201,26 +201,26 @@ func (p *Pod) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error)
}
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
return append(outs, tailLogs(ctx, p, opts)), nil
}
if opts.HasContainer() && !opts.AllContainers {
return append(outs, tailLogs(ctx, p, opts)), nil
}
for _, co := range po.Spec.InitContainers {
for i := range po.Spec.InitContainers {
cfg := opts.Clone()
cfg.Container = co.Name
cfg.Container = po.Spec.InitContainers[i].Name
outs = append(outs, tailLogs(ctx, p, cfg))
}
for _, co := range po.Spec.Containers {
for i := range po.Spec.Containers {
cfg := opts.Clone()
cfg.Container = co.Name
cfg.Container = po.Spec.Containers[i].Name
outs = append(outs, tailLogs(ctx, p, cfg))
}
for _, co := range po.Spec.EphemeralContainers {
for i := range po.Spec.EphemeralContainers {
cfg := opts.Clone()
cfg.Container = co.Name
cfg.Container = po.Spec.EphemeralContainers[i].Name
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.
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)
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 {
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.
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)
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 {
return nil, err
}
@ -277,7 +277,7 @@ func (p *Pod) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (
continue
}
switch gvr {
case CmGVR:
case client.CmGVR:
if !hasConfigMap(&pod.Spec, n) {
continue
}
@ -285,7 +285,7 @@ func (p *Pod) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (
GVR: p.GVR(),
FQN: client.FQN(pod.Namespace, pod.Name),
})
case SecGVR:
case client.SecGVR:
found, err := hasSecret(p.Factory, &pod.Spec, pod.Namespace, n, wait)
if err != nil {
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(),
FQN: client.FQN(pod.Namespace, pod.Name),
})
case PvcGVR:
case client.PvcGVR:
if !hasPVC(&pod.Spec, n) {
continue
}
@ -309,7 +309,7 @@ func (p *Pod) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (
GVR: p.GVR(),
FQN: client.FQN(pod.Namespace, pod.Name),
})
case PcGVR:
case client.PcGVR:
if !hasPC(&pod.Spec, n) {
continue
}
@ -336,20 +336,20 @@ func tailLogs(ctx context.Context, logger Logger, opts *LogOptions) LogChan {
go func() {
defer wg.Done()
podOpts := opts.ToPodLogOptions()
for r := 0; r < logRetryCount; r++ {
for range logRetryCount {
req, err := logger.Logs(opts.Path, podOpts)
if err == nil {
// 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)
go readLogs(ctx, &wg, stream, out, opts)
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 {
slog.Error("Log request failed",
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.
func MetaFQN(m metav1.ObjectMeta) string {
func MetaFQN(m *metav1.ObjectMeta) string {
if m.Namespace == "" {
return m.Name
}
@ -434,13 +434,14 @@ func (p *Pod) GetPodSpec(path string) (*v1.PodSpec, error) {
return nil, err
}
podSpec := pod.Spec
return &podSpec, nil
}
// SetImages sets container images.
func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
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 {
return err
}
@ -469,10 +470,11 @@ func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs)
jsonPatch,
metav1.PatchOptions{},
)
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)
if err != nil {
return "", false, err
@ -481,6 +483,7 @@ func (p *Pod) isControlled(path string) (string, bool, error) {
if len(references) > 0 {
return fmt.Sprintf("%s/%s", references[0].Kind, references[0].Name), true, nil
}
return "", false, nil
}

View File

@ -51,7 +51,7 @@ func TestGetDefaultContainer(t *testing.T) {
for k := range uu {
u := uu[k]
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.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()
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 {
return nil, err
}
@ -132,7 +132,7 @@ func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.Por
podName := strings.Split(n, "|")[0]
var res Pod
res.Init(p, client.NewGVR("v1/pods"))
res.Init(p, client.PodGVR)
pod, err := res.GetInstance(client.FQN(ns, podName))
if err != nil {
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)
}
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 {
return nil, err
}

View File

@ -16,6 +16,6 @@ type Pulse struct {
}
// 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")
}

View File

@ -18,13 +18,6 @@ import (
"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 (
_ Accessor = (*Rbac)(nil)
_ Nuker = (*Rbac)(nil)
@ -37,7 +30,7 @@ type Rbac struct {
// List lists out rbac resources.
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 {
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) {
o, err := r.getFactory().Get(crbGVR, path, true, labels.Everything())
crbo, err := r.getFactory().Get(client.CrbGVR, path, true, labels.Everything())
if err != nil {
return nil, err
}
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 {
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 {
return nil, err
}
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 {
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) {
o, err := r.getFactory().Get(rbGVR, path, true, labels.Everything())
rbo, err := r.getFactory().Get(client.RobGVR, path, true, labels.Everything())
if err != nil {
return nil, err
}
var rb rbacv1.RoleBinding
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb); err != nil {
return nil, err
if e := runtime.DefaultUnstructuredConverter.FromUnstructured(rbo.(*unstructured.Unstructured).Object, &rb); e != nil {
return nil, e
}
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 {
return nil, e
}
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 {
return nil, err
}
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 {
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) {
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 {
return nil, err
}
var cr rbacv1.ClusterRole
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr)
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) {
o, err := r.getFactory().Get(rGVR, path, true, labels.Everything())
o, err := r.getFactory().Get(client.RoGVR, path, true, labels.Everything())
if err != nil {
return nil, err
}
var ro rbacv1.Role
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ro)
if err != nil {

View File

@ -29,7 +29,7 @@ type Policy struct {
}
// 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)
if !ok {
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)
var nn []string
for _, crb := range crbs {
for _, s := range crb.Subjects {
s := s
for i := range crbs {
for _, s := range crbs[i].Subjects {
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))
for _, cr := range crs {
if !inList(nn, cr.Name) {
for i := range crs {
if !inList(nn, crs[i].Name) {
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
@ -101,13 +100,13 @@ func (p *Policy) loadRoleBinding(kind, name string) (render.Policies, error) {
return nil, err
}
rows := make(render.Policies, 0, len(crs))
for _, cr := range crs {
if rbNs, ok := rbsMap["ClusterRole:"+cr.Name]; ok {
for i := range crs {
if rbNs, ok := rbsMap["ClusterRole:"+crs[i].Name]; ok {
slog.Debug("Loading rules for clusterrole",
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 {
return nil, err
}
for _, ro := range ros {
if _, ok := rbsMap["Role:"+ro.Name]; !ok {
for i := range ros {
if _, ok := rbsMap["Role:"+ros[i].Name]; !ok {
continue
}
slog.Debug("Loading rules for role",
slogs.Namespace, ro.Namespace,
slogs.ResName, ro.Name,
slogs.Namespace, ros[i].Namespace,
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
}
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 {
return nil, err
}
@ -148,7 +147,7 @@ func fetchClusterRoleBindings(f Factory) ([]rbacv1.ClusterRoleBinding, 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 {
return nil, err
}
@ -173,11 +172,10 @@ func (p *Policy) fetchRoleBindingNamespaces(kind, name string) (map[string]strin
ns, n := client.Namespaced(name)
ss := make(map[string]string, len(rbs))
for _, rb := range rbs {
for _, s := range rb.Subjects {
s := s
for i := range rbs {
for _, s := range rbs[i].Subjects {
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) {
const gvr = "rbac.authorization.k8s.io/v1/clusterroles"
oo, err := p.getFactory().List(gvr, client.ClusterScope, false, labels.Everything())
oo, err := p.getFactory().List(client.CrGVR, client.ClusterScope, false, labels.Everything())
if err != nil {
return nil, err
}
@ -220,9 +216,7 @@ func (p *Policy) fetchClusterRoles() ([]rbacv1.ClusterRole, error) {
}
func (p *Policy) fetchRoles() ([]rbacv1.Role, error) {
const gvr = "rbac.authorization.k8s.io/v1/roles"
oo, err := p.getFactory().List(gvr, client.BlankNamespace, false, labels.Everything())
oo, err := p.getFactory().List(client.RoGVR, client.BlankNamespace, false, labels.Everything())
if err != nil {
return nil, err
}

View File

@ -23,7 +23,7 @@ type Subject struct {
}
// 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)
if !ok {
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))
for _, crb := range crbs {
for _, su := range crb.Subjects {
for i := range crbs {
for _, su := range crbs[i].Subjects {
if su.Kind != kind {
continue
}
oo = oo.Upsert(render.SubjectRes{
Name: su.Name,
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))
for _, rb := range rbs {
for _, su := range rb.Subjects {
for i := range rbs {
for _, su := range rbs[i].Subjects {
if su.Kind != kind {
continue
}
oo = oo.Upsert(render.SubjectRes{
Name: su.Name,
Kind: "RoleBinding",
FirstLocation: rb.Name,
FirstLocation: rbs[i].Name,
})
}
}

View File

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

View File

@ -6,8 +6,8 @@ package dao
import (
"fmt"
"log/slog"
"maps"
"slices"
"sort"
"strings"
"sync"
@ -27,12 +27,8 @@ const (
k9sCat = "k9s"
helmCat = "helm"
scaleCat = "scale"
crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
)
// MetaAccess tracks resources metadata.
var MetaAccess = NewMeta()
var stdGroups = sets.New[string](
"apps/v1",
"autoscaling/v1",
@ -47,12 +43,18 @@ var stdGroups = sets.New[string](
"v1",
)
// ResourceMetas represents a collection of resource metadata.
type ResourceMetas map[*client.GVR]*metav1.APIResource
func (m ResourceMetas) clear() {
for k := range m {
delete(m, k)
}
}
// MetaAccess tracks resources metadata.
var MetaAccess = NewMeta()
// Meta represents available resource metas.
type Meta struct {
resMetas ResourceMetas
@ -64,71 +66,40 @@ func NewMeta() *Meta {
return &Meta{resMetas: make(ResourceMetas)}
}
// 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) {
m := Accessors{
client.NewGVR("workloads"): &Workload{},
client.NewGVR("contexts"): &Context{},
client.NewGVR("containers"): &Container{},
client.NewGVR("scans"): &ImageScan{},
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{},
func (m *Meta) Lookup(cmd string) *client.GVR {
m.mx.RLock()
defer m.mx.RUnlock()
for gvr, meta := range m.resMetas {
if slices.Contains(meta.ShortNames, cmd) {
return gvr
}
if meta.Name == cmd || meta.SingularName == cmd || meta.Kind == cmd {
return gvr
}
}
r, ok := m[gvr]
if !ok {
r = new(Scaler)
slog.Debug("No DAO registry entry. Using generics!", slogs.GVR, gvr)
}
r.Init(f, gvr)
return r, nil
return client.NoGVR
}
// 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()
defer m.mx.Unlock()
m.resMetas[client.NewGVR(gvr)] = res
}
// AllGVRs returns all cluster resources.
// AllGVRs returns all sorted cluster resources.
func (m *Meta) AllGVRs() client.GVRs {
m.mx.RLock()
defer m.mx.RUnlock()
kk := slices.Collect(maps.Keys(m.resMetas))
kk := make(client.GVRs, 0, len(m.resMetas))
for k := range m.resMetas {
kk = append(kk, k)
}
sort.Sort(kk)
return kk
return client.GVRs(kk)
}
// 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()
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.
func (m *Meta) MetaFor(gvr client.GVR) (metav1.APIResource, error) {
func (m *Meta) MetaFor(gvr *client.GVR) (*metav1.APIResource, error) {
m.mx.RLock()
defer m.mx.RUnlock()
meta, ok := m.resMetas[gvr]
if !ok {
return metav1.APIResource{}, fmt.Errorf("no resource meta defined for %q", gvr)
if meta, ok := m.resMetas[gvr]; ok {
return meta, nil
}
return meta, nil
return new(metav1.APIResource), fmt.Errorf("no resource meta defined for\n %q", gvr)
}
// 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)
}
// 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 category == k9sCat || category == helmCat
})
}
// IsK9sMeta checks for non resource meta.
func IsK9sMeta(m metav1.APIResource) bool {
func IsK9sMeta(m *metav1.APIResource) bool {
return slices.Contains(m.Categories, k9sCat)
}
// 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)
}
@ -201,7 +172,7 @@ func loadNonResource(m ResourceMetas) {
}
func loadK9s(m ResourceMetas) {
m[client.NewGVR("workloads")] = metav1.APIResource{
m[client.WkGVR] = &metav1.APIResource{
Name: "workloads",
Kind: "Workload",
SingularName: "workload",
@ -209,48 +180,48 @@ func loadK9s(m ResourceMetas) {
ShortNames: []string{"wk"},
Categories: []string{k9sCat},
}
m[client.NewGVR("pulses")] = metav1.APIResource{
m[client.PuGVR] = &metav1.APIResource{
Name: "pulses",
Kind: "Pulse",
SingularName: "pulses",
SingularName: "pulse",
ShortNames: []string{"hz", "pu"},
Categories: []string{k9sCat},
}
m[client.NewGVR("dir")] = metav1.APIResource{
Name: "dir",
m[client.DirGVR] = &metav1.APIResource{
Name: "dirs",
Kind: "Dir",
SingularName: "dir",
Categories: []string{k9sCat},
}
m[client.NewGVR("xrays")] = metav1.APIResource{
Name: "xray",
m[client.XGVR] = &metav1.APIResource{
Name: "xrays",
Kind: "XRays",
SingularName: "xray",
Categories: []string{k9sCat},
}
m[client.NewGVR("references")] = metav1.APIResource{
m[client.RefGVR] = &metav1.APIResource{
Name: "references",
Kind: "References",
SingularName: "reference",
Verbs: []string{},
Categories: []string{k9sCat},
}
m[client.NewGVR("aliases")] = metav1.APIResource{
m[client.AliGVR] = &metav1.APIResource{
Name: "aliases",
Kind: "Aliases",
SingularName: "alias",
Verbs: []string{},
Categories: []string{k9sCat},
}
m[client.NewGVR("contexts")] = metav1.APIResource{
Name: "contexts",
m[client.CtGVR] = &metav1.APIResource{
Name: client.CtGVR.String(),
Kind: "Contexts",
SingularName: "context",
ShortNames: []string{"ctx"},
Verbs: []string{},
Categories: []string{k9sCat},
}
m[client.NewGVR("screendumps")] = metav1.APIResource{
m[client.SdGVR] = &metav1.APIResource{
Name: "screendumps",
Kind: "ScreenDumps",
SingularName: "screendump",
@ -258,7 +229,7 @@ func loadK9s(m ResourceMetas) {
Verbs: []string{"delete"},
Categories: []string{k9sCat},
}
m[client.NewGVR("benchmarks")] = metav1.APIResource{
m[client.BeGVR] = &metav1.APIResource{
Name: "benchmarks",
Kind: "Benchmarks",
SingularName: "benchmark",
@ -266,7 +237,7 @@ func loadK9s(m ResourceMetas) {
Verbs: []string{"delete"},
Categories: []string{k9sCat},
}
m[client.NewGVR("portforwards")] = metav1.APIResource{
m[client.PfGVR] = &metav1.APIResource{
Name: "portforwards",
Namespaced: true,
Kind: "PortForwards",
@ -275,14 +246,14 @@ func loadK9s(m ResourceMetas) {
Verbs: []string{"delete"},
Categories: []string{k9sCat},
}
m[client.NewGVR("containers")] = metav1.APIResource{
m[client.CoGVR] = &metav1.APIResource{
Name: "containers",
Kind: "Containers",
SingularName: "container",
Verbs: []string{},
Categories: []string{k9sCat},
}
m[client.NewGVR("scans")] = metav1.APIResource{
m[client.ScnGVR] = &metav1.APIResource{
Name: "scans",
Kind: "Scans",
SingularName: "scan",
@ -292,14 +263,14 @@ func loadK9s(m ResourceMetas) {
}
func loadHelm(m ResourceMetas) {
m[client.NewGVR("helm")] = metav1.APIResource{
m[client.HmGVR] = &metav1.APIResource{
Name: "helm",
Kind: "Helm",
Namespaced: true,
Verbs: []string{"delete"},
Categories: []string{helmCat},
}
m[client.NewGVR("helm-history")] = metav1.APIResource{
m[client.HmhGVR] = &metav1.APIResource{
Name: "history",
Kind: "History",
Namespaced: true,
@ -309,23 +280,23 @@ func loadHelm(m ResourceMetas) {
}
func loadRBAC(m ResourceMetas) {
m[client.NewGVR("rbac")] = metav1.APIResource{
m[client.RbacGVR] = &metav1.APIResource{
Name: "rbacs",
Kind: "Rules",
Categories: []string{k9sCat},
}
m[client.NewGVR("policy")] = metav1.APIResource{
m[client.PolGVR] = &metav1.APIResource{
Name: "policies",
Kind: "Rules",
Namespaced: true,
Categories: []string{k9sCat},
}
m[client.NewGVR("users")] = metav1.APIResource{
m[client.UsrGVR] = &metav1.APIResource{
Name: "users",
Kind: "User",
Categories: []string{k9sCat},
}
m[client.NewGVR("groups")] = metav1.APIResource{
m[client.GrpGVR] = &metav1.APIResource{
Name: "groups",
Kind: "Group",
Categories: []string{k9sCat},
@ -333,7 +304,7 @@ func loadRBAC(m ResourceMetas) {
}
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")
return nil
}
@ -347,7 +318,8 @@ func loadPreferred(f Factory, m ResourceMetas) error {
slog.Debug("Failed to load preferred resources", slogs.Error, err)
}
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)
if isDeprecated(gvr) {
continue
@ -359,7 +331,7 @@ func loadPreferred(f Factory, m ResourceMetas) error {
if !isStandardGroup(r.GroupVersion) {
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")
}
var deprecatedGVRs = sets.New[client.GVR](
var deprecatedGVRs = sets.New(
client.NewGVR("v1/events"),
client.NewGVR("extensions/v1beta1/ingresses"),
)
func isDeprecated(gvr client.GVR) bool {
return deprecatedGVRs.Has(gvr)
func isDeprecated(gvr *client.GVR) bool {
return deprecatedGVRs.Has(gvr) || gvr.V() == ""
}
// loadCRDs Wait for the cache to synced and then add some additional properties to CRD.
func loadCRDs(f Factory, m ResourceMetas) {
if f.Client() == nil || !f.Client().ConnectionOK() {
if f == nil || f.Client() == nil || !f.Client().ConnectionOK() {
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 {
slog.Warn("CRDs load Fail", slogs.Error, err)
return
@ -397,125 +370,13 @@ func loadCRDs(f Factory, m ResourceMetas) {
slog.Error("CRD conversion failed", slogs.Error, err)
continue
}
gvr, version, ok := newGVRFromCRD(&crd)
if !ok {
continue
}
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
for gvr, version := range client.NewGVRFromCRD(&crd) {
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
import (
"encoding/json"
"fmt"
"os"
"errors"
"testing"
"github.com/derailed/k9s/internal/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestExtractMeta(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) {
func TestMetaFor(t *testing.T) {
uu := map[string]struct {
m map[string]interface{}
n string
nn []string
ee []error
gvr *client.GVR
err error
e metav1.APIResource
}{
"plain": {
m: map[string]interface{}{"shortNames": []string{"a", "b", "c"}},
n: "shortNames",
nn: []string{"a", "b", "c"},
"xray-gvr": {
gvr: client.XGVR,
e: metav1.APIResource{
Name: "xrays",
Kind: "XRays",
SingularName: "xray",
Categories: []string{k9sCat},
},
},
"empty": {
m: map[string]interface{}{},
n: "shortNames",
"xray": {
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 {
u := uu[k]
t.Run(k, func(t *testing.T) {
ss, e := extractSlice(u.m, u.n, ee)
assert.Equal(t, u.ee, e)
assert.Equal(t, u.nn, ss)
meta, err := m.MetaFor(u.gvr)
assert.Equal(t, u.err, err)
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.
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.

View File

@ -65,11 +65,12 @@ func (r *RestMapper) resourceFor(resourceArg string) (schema.GroupVersionResourc
gvr, err = mapper.ResourceFor(gr.WithVersion(""))
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' in group '%s'", gr.Resource, gr.Group)
}
return gvr, nil
}

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