diff --git a/.github/workflows/stales.yml b/.github/workflows/stales-issues.yml similarity index 60% rename from .github/workflows/stales.yml rename to .github/workflows/stales-issues.yml index 6d033c2d..5b371568 100644 --- a/.github/workflows/stales.yml +++ b/.github/workflows/stales-issues.yml @@ -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 }} \ No newline at end of file diff --git a/.github/workflows/stales-prs.yml b/.github/workflows/stales-prs.yml new file mode 100644 index 00000000..33d7b8aa --- /dev/null +++ b/.github/workflows/stales-prs.yml @@ -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 }} \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 94113d84..1bd7424b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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 \ No newline at end of file +# # - 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 \ No newline at end of file diff --git a/Makefile b/Makefile index 7f72af67..2a42a91a 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H: else DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") endif -VERSION ?= v0.40.10 +VERSION ?= v0.50.0 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} diff --git a/change_logs/release_v0.50.0.md b/change_logs/release_v0.50.0.md new file mode 100644 index 00000000..d1c71540 --- /dev/null +++ b/change_logs/release_v0.50.0.md @@ -0,0 +1,93 @@ + + +# 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 + +--- + + ยฉ 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/cmd/info.go b/cmd/info.go index 44a7e30f..b439d680 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -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 } diff --git a/cmd/root.go b/cmd/root.go index ab76e732..d68a83a1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 { diff --git a/cmd/version.go b/cmd/version.go index 65298b9d..f4c2244c 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -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) }, } diff --git a/go.mod b/go.mod index 3d16a30f..9d944a4d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 58084637..94bd74c7 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/client/client.go b/internal/client/client.go index 2b681ebf..59898396 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -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 { diff --git a/internal/client/client_test.go b/internal/client/client_test.go index e6063488..df84cb41 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -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": { diff --git a/internal/client/config.go b/internal/client/config.go index ebbf8bc5..83597eca 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -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 } diff --git a/internal/client/config_test.go b/internal/client/config_test.go index 5e501d3d..f0d7d72a 100644 --- a/internal/client/config_test.go +++ b/internal/client/config_test.go @@ -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 diff --git a/internal/client/gvr.go b/internal/client/gvr.go index 1d4a4b68..f1ea041c 100644 --- a/internal/client/gvr.go +++ b/internal/client/gvr.go @@ -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 { diff --git a/internal/client/gvr_test.go b/internal/client/gvr_test.go index fb310df2..6f449c16 100644 --- a/internal/client/gvr_test.go +++ b/internal/client/gvr_test.go @@ -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": {""}, } diff --git a/internal/client/gvrs.go b/internal/client/gvrs.go new file mode 100644 index 00000000..eae47ad7 --- /dev/null +++ b/internal/client/gvrs.go @@ -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") +) diff --git a/internal/client/helper_test.go b/internal/client/helper_test.go index 5cd46519..3b4ccd17 100644 --- a/internal/client/helper_test.go +++ b/internal/client/helper_test.go @@ -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)) }) } } diff --git a/internal/client/helpers.go b/internal/client/helpers.go index d2dbabd5..116e6145 100644 --- a/internal/client/helpers.go +++ b/internal/client/helpers.go @@ -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, "_") } diff --git a/internal/client/metrics.go b/internal/client/metrics.go index f79aeac4..b239db68 100644 --- a/internal/client/metrics.go +++ b/internal/client/metrics.go @@ -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 } } diff --git a/internal/client/metrics_test.go b/internal/client/metrics_test.go index 8a433c86..e0b84d28 100644 --- a/internal/client/metrics_test.go +++ b/internal/client/metrics_test.go @@ -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) } } diff --git a/internal/client/types.go b/internal/client/types.go index b5a7d3cf..e28f3e2e 100644 --- a/internal/client/types.go +++ b/internal/client/types.go @@ -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. diff --git a/internal/color/colorize_test.go b/internal/color/colorize_test.go index 3d71f83f..b2cd5dd2 100644 --- a/internal/color/colorize_test.go +++ b/internal/color/colorize_test.go @@ -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))) }) } } diff --git a/internal/config/alias.go b/internal/config/alias.go index 6503c493..05d85cdd 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -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) } diff --git a/internal/config/alias_test.go b/internal/config/alias_test.go index c67f4f58..9391663d 100644 --- a/internal/config/alias_test.go +++ b/internal/config/alias_test.go @@ -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 } diff --git a/internal/config/benchmark_test.go b/internal/config/benchmark_test.go index cd80d442..c9f272fa 100644 --- a/internal/config/benchmark_test.go +++ b/internal/config/benchmark_test.go @@ -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) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a8db98ec..031eb742 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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)) } diff --git a/internal/config/data/context.go b/internal/config/data/context.go index 15021033..060e01eb 100644 --- a/internal/config/data/context.go +++ b/internal/config/data/context.go @@ -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() diff --git a/internal/config/data/context_test.go b/internal/config/data/context_test.go index 30318ead..8f313df5 100644 --- a/internal/config/data/context_test.go +++ b/internal/config/data/context_test.go @@ -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) } diff --git a/internal/config/data/dir_test.go b/internal/config/data/dir_test.go index 4d29943b..0ef36d9a 100644 --- a/internal/config/data/dir_test.go +++ b/internal/config/data/dir_test.go @@ -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 } diff --git a/internal/config/data/helpers.go b/internal/config/data/helpers.go index beae284a..d2bce306 100644 --- a/internal/config/data/helpers.go +++ b/internal/config/data/helpers.go @@ -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 } } diff --git a/internal/config/data/helpers_test.go b/internal/config/data/helpers_test.go index fc7b6cfa..b81895ea 100644 --- a/internal/config/data/helpers_test.go +++ b/internal/config/data/helpers_test.go @@ -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()) } diff --git a/internal/config/data/ns.go b/internal/config/data/ns.go index f37488f1..b345b9a2 100644 --- a/internal/config/data/ns.go +++ b/internal/config/data/ns.go @@ -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]) } diff --git a/internal/config/data/ns_test.go b/internal/config/data/ns_test.go index 5d39cd37..16453835 100644 --- a/internal/config/data/ns_test.go +++ b/internal/config/data/ns_test.go @@ -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) } diff --git a/internal/config/data/view.go b/internal/config/data/view.go index 044972eb..7c77b467 100644 --- a/internal/config/data/view.go +++ b/internal/config/data/view.go @@ -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 } } diff --git a/internal/config/files.go b/internal/config/files.go index c3d2f675..6e806255 100644 --- a/internal/config/files.go +++ b/internal/config/files.go @@ -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")) diff --git a/internal/config/files_int_test.go b/internal/config/files_int_test.go index c6094ead..cdcb00e4 100644 --- a/internal/config/files_int_test.go +++ b/internal/config/files_int_test.go @@ -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) }) } diff --git a/internal/config/files_test.go b/internal/config/files_test.go index 99da056a..2356e110 100644 --- a/internal/config/files_test.go +++ b/internal/config/files_test.go @@ -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 diff --git a/internal/config/helpers.go b/internal/config/helpers.go index 08b5a174..b56c9816 100644 --- a/internal/config/helpers.go +++ b/internal/config/helpers.go @@ -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 { diff --git a/internal/config/hotkey_test.go b/internal/config/hotkey_test.go index 66c986af..6ee93c97 100644 --- a/internal/config/hotkey_test.go +++ b/internal/config/hotkey_test.go @@ -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) } diff --git a/internal/config/json/validator_test.go b/internal/config/json/validator_test.go index 7b78f251..79e74d2b 100644 --- a/internal/config/json/validator_test.go +++ b/internal/config/json/validator_test.go @@ -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()) } diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 55c89df3..df2752de 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -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() diff --git a/internal/config/k9s_int_test.go b/internal/config/k9s_int_test.go index c6113e10..a2b18ec5 100644 --- a/internal/config/k9s_int_test.go +++ b/internal/config/k9s_int_test.go @@ -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()) diff --git a/internal/config/k9s_test.go b/internal/config/k9s_test.go index d74c59c3..6afc155a 100644 --- a/internal/config/k9s_test.go +++ b/internal/config/k9s_test.go @@ -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()) } diff --git a/internal/config/mock/test_helpers.go b/internal/config/mock/test_helpers.go index 0f92ab0f..97ab1f65 100644 --- a/internal/config/mock/test_helpers.go +++ b/internal/config/mock/test_helpers.go @@ -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 } diff --git a/internal/config/plugin.go b/internal/config/plugin.go index 4d23d0ff..cc41c12f 100644 --- a/internal/config/plugin.go +++ b/internal/config/plugin.go @@ -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] } } diff --git a/internal/config/plugin_test.go b/internal/config/plugin_test.go index 3bbbc08a..2de77bbb 100644 --- a/internal/config/plugin_test.go +++ b/internal/config/plugin_test.go @@ -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) }) } diff --git a/internal/config/shell_pod.go b/internal/config/shell_pod.go index 08540f2c..43682bcd 100644 --- a/internal/config/shell_pod.go +++ b/internal/config/shell_pod.go @@ -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 { diff --git a/internal/config/styles.go b/internal/config/styles.go index bdbce4d3..9811ff4e 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -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")}, }, } } diff --git a/internal/config/styles_test.go b/internal/config/styles_test.go index b2ada278..7f754463 100644 --- a/internal/config/styles_test.go +++ b/internal/config/styles_test.go @@ -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()) diff --git a/internal/config/threshold.go b/internal/config/threshold.go index 0e1486bf..01046209 100644 --- a/internal/config/threshold.go +++ b/internal/config/threshold.go @@ -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" diff --git a/internal/config/threshold_test.go b/internal/config/threshold_test.go index 11cabae3..360bff71 100644 --- a/internal/config/threshold_test.go +++ b/internal/config/threshold_test.go @@ -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, }, diff --git a/internal/config/types.go b/internal/config/types.go index f40f93c4..2d3e5c60 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -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. diff --git a/internal/config/views.go b/internal/config/views.go index 5fdd6820..88433711 100644 --- a/internal/config/views.go +++ b/internal/config/views.go @@ -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 diff --git a/internal/config/views_int_test.go b/internal/config/views_int_test.go index e9eb0e11..8ee7cca0 100644 --- a/internal/config/views_int_test.go +++ b/internal/config/views_int_test.go @@ -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)) diff --git a/internal/config/views_test.go b/internal/config/views_test.go index b5e22cb0..01dd94bf 100644 --- a/internal/config/views_test.go +++ b/internal/config/views_test.go @@ -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) }) } diff --git a/internal/dao/accessor.go b/internal/dao/accessor.go new file mode 100644 index 00000000..5069cdc0 --- /dev/null +++ b/internal/dao/accessor.go @@ -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 +} diff --git a/internal/dao/alias.go b/internal/dao/alias.go index a1295286..c6f48b20 100644 --- a/internal/dao/alias.go +++ b/internal/dao/alias.go @@ -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 diff --git a/internal/dao/alias_test.go b/internal/dao/alias_test.go index d8edd96e..0d59ee08 100644 --- a/internal/dao/alias_test.go +++ b/internal/dao/alias_test.go @@ -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, }, }, } diff --git a/internal/dao/benchmark.go b/internal/dao/benchmark.go index 683cb2a4..e6ab04c8 100644 --- a/internal/dao/benchmark.go +++ b/internal/dao/benchmark.go @@ -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") diff --git a/internal/dao/benchmark_test.go b/internal/dao/benchmark_test.go index 12fed066..e5497779 100644 --- a/internal/dao/benchmark_test.go +++ b/internal/dao/benchmark_test.go @@ -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) } diff --git a/internal/dao/cluster.go b/internal/dao/cluster.go index 8b0b1b7c..363641b2 100644 --- a/internal/dao/cluster.go +++ b/internal/dao/cluster.go @@ -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() { diff --git a/internal/dao/container.go b/internal/dao/container.go index 14b20344..7ef72b0d 100644 --- a/internal/dao/container.go +++ b/internal/dao/container.go @@ -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) } diff --git a/internal/dao/container_test.go b/internal/dao/container_test.go index f53c8295..4b916ba3 100644 --- a/internal/dao/container_test.go +++ b/internal/dao/container_test.go @@ -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{} diff --git a/internal/dao/context.go b/internal/dao/context.go index de655be2..2800505f 100644 --- a/internal/dao/context.go +++ b/internal/dao/context.go @@ -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 diff --git a/internal/dao/cronjob.go b/internal/dao/cronjob.go index ba5514c3..98497e93 100644 --- a/internal/dao/cronjob.go +++ b/internal/dao/cronjob.go @@ -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 = ¤t } 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 } diff --git a/internal/dao/cruiser.go b/internal/dao/cruiser.go index 25a194ee..f9221ba1 100644 --- a/internal/dao/cruiser.go +++ b/internal/dao/cruiser.go @@ -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)) diff --git a/internal/dao/cruiser_test.go b/internal/dao/cruiser_test.go index 6e88cb86..05d2021d 100644 --- a/internal/dao/cruiser_test.go +++ b/internal/dao/cruiser_test.go @@ -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 } diff --git a/internal/dao/describe.go b/internal/dao/describe.go index 541f453c..6e07f3b2 100644 --- a/internal/dao/describe.go +++ b/internal/dao/describe.go @@ -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 { diff --git a/internal/dao/dir.go b/internal/dao/dir.go index ef9c67ba..bff00c2d 100644 --- a/internal/dao/dir.go +++ b/internal/dao/dir.go @@ -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") } diff --git a/internal/dao/dir_test.go b/internal/dao/dir_test.go index c33e5d78..a0ba3a96 100644 --- a/internal/dao/dir_test.go +++ b/internal/dao/dir_test.go @@ -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) } diff --git a/internal/dao/dp.go b/internal/dao/dp.go index 61d2695f..57b4aa52 100644 --- a/internal/dao/dp.go +++ b/internal/dao/dp.go @@ -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 +} diff --git a/internal/dao/ds.go b/internal/dao/ds.go index dcbadaed..1e3a7c25 100644 --- a/internal/dao/ds.go +++ b/internal/dao/ds.go @@ -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 } diff --git a/internal/dao/dynamic.go b/internal/dao/dynamic.go index de97b268..9e2293b3 100644 --- a/internal/dao/dynamic.go +++ b/internal/dao/dynamic.go @@ -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 diff --git a/internal/dao/generic.go b/internal/dao/generic.go index e7d95ccc..c5c4f6ff 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -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 } diff --git a/internal/dao/helm_chart.go b/internal/dao/helm_chart.go index 43c712b9..5ba52e07 100644 --- a/internal/dao/helm_chart.go +++ b/internal/dao/helm_chart.go @@ -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", diff --git a/internal/dao/helm_history.go b/internal/dao/helm_history.go index bae7ca28..10900ee4 100644 --- a/internal/dao/helm_history.go +++ b/internal/dao/helm_history.go @@ -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 diff --git a/internal/dao/helpers.go b/internal/dao/helpers.go index 63d4c0a5..1bf6531b 100644 --- a/internal/dao/helpers.go +++ b/internal/dao/helpers.go @@ -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") } } diff --git a/internal/dao/helpers_test.go b/internal/dao/helpers_test.go index f201654b..23dcec14 100644 --- a/internal/dao/helpers_test.go +++ b/internal/dao/helpers_test.go @@ -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)) } } diff --git a/internal/dao/img_scan.go b/internal/dao/img_scan.go index 4db8705b..3a5daa45 100644 --- a/internal/dao/img_scan.go +++ b/internal/dao/img_scan.go @@ -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) } diff --git a/internal/dao/job.go b/internal/dao/job.go index eca8f6c9..c44dab2e 100644 --- a/internal/dao/job.go +++ b/internal/dao/job.go @@ -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 } diff --git a/internal/dao/log_item.go b/internal/dao/log_item.go index ff90bbff..0c8fe230 100644 --- a/internal/dao/log_item.go +++ b/internal/dao/log_item.go @@ -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("[-::] ") } diff --git a/internal/dao/log_item_test.go b/internal/dao/log_item_test.go index f805f96f..c25878a4 100644 --- a/internal/dao/log_item_test.go +++ b/internal/dao/log_item_test.go @@ -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) } diff --git a/internal/dao/log_items.go b/internal/dao/log_items.go index 898c0c84..c5ad94f8 100644 --- a/internal/dao/log_items.go +++ b/internal/dao/log_items.go @@ -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 { diff --git a/internal/dao/log_options.go b/internal/dao/log_options.go index edd20afb..40824bdb 100644 --- a/internal/dao/log_options.go +++ b/internal/dao/log_options.go @@ -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 diff --git a/internal/dao/node.go b/internal/dao/node.go index 68a30ec6..048f3695 100644 --- a/internal/dao/node.go +++ b/internal/dao/node.go @@ -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 } diff --git a/internal/dao/non_resource.go b/internal/dao/non_resource.go index b7b1e6f3..b407e5a4 100644 --- a/internal/dao/non_resource.go +++ b/internal/dao/non_resource.go @@ -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") } diff --git a/internal/dao/patch.go b/internal/dao/patch.go index e834f196..b678c25c 100644 --- a/internal/dao/patch.go +++ b/internal/dao/patch.go @@ -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}) diff --git a/internal/dao/pod.go b/internal/dao/pod.go index d402999d..c32e22c3 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -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 } diff --git a/internal/dao/pod_test.go b/internal/dao/pod_test.go index f6f1593c..796267c8 100644 --- a/internal/dao/pod_test.go +++ b/internal/dao/pod_test.go @@ -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) }) diff --git a/internal/dao/port_forwarder.go b/internal/dao/port_forwarder.go index c42bbb69..bd96451d 100644 --- a/internal/dao/port_forwarder.go +++ b/internal/dao/port_forwarder.go @@ -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 } diff --git a/internal/dao/pulse.go b/internal/dao/pulse.go index b7d09c7b..15d4f94e 100644 --- a/internal/dao/pulse.go +++ b/internal/dao/pulse.go @@ -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") } diff --git a/internal/dao/rbac.go b/internal/dao/rbac.go index afcd4ce6..2e078e2e 100644 --- a/internal/dao/rbac.go +++ b/internal/dao/rbac.go @@ -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 { diff --git a/internal/dao/rbac_policy.go b/internal/dao/rbac_policy.go index 067b45b3..4dd89bff 100644 --- a/internal/dao/rbac_policy.go +++ b/internal/dao/rbac_policy.go @@ -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 } diff --git a/internal/dao/rbac_subject.go b/internal/dao/rbac_subject.go index 0939b50a..056af41b 100644 --- a/internal/dao/rbac_subject.go +++ b/internal/dao/rbac_subject.go @@ -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, }) } } diff --git a/internal/dao/reference.go b/internal/dao/reference.go index 3ea129f6..ac84015a 100644 --- a/internal/dao/reference.go +++ b/internal/dao/reference.go @@ -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") } diff --git a/internal/dao/registry.go b/internal/dao/registry.go index bcc725f7..bd9e5edf 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -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 -} diff --git a/internal/dao/registry_test.go b/internal/dao/registry_test.go index 51a05dba..8d58a44d 100644 --- a/internal/dao/registry_test.go +++ b/internal/dao/registry_test.go @@ -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 -} diff --git a/internal/dao/resource.go b/internal/dao/resource.go index d704c4b5..ea7a4695 100644 --- a/internal/dao/resource.go +++ b/internal/dao/resource.go @@ -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. diff --git a/internal/dao/rest_mapper.go b/internal/dao/rest_mapper.go index d9e6f8e3..0a7d60c0 100644 --- a/internal/dao/rest_mapper.go +++ b/internal/dao/rest_mapper.go @@ -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 } diff --git a/internal/dao/rs.go b/internal/dao/rs.go index 9e7a46e7..6bf691a1 100644 --- a/internal/dao/rs.go +++ b/internal/dao/rs.go @@ -31,7 +31,7 @@ type ReplicaSet struct { } // ListImages lists container images. -func (r *ReplicaSet) ListImages(ctx context.Context, fqn string) ([]string, error) { +func (r *ReplicaSet) ListImages(_ context.Context, fqn string) ([]string, error) { rs, err := r.Load(r.Factory, fqn) if err != nil { return nil, err @@ -41,8 +41,8 @@ func (r *ReplicaSet) ListImages(ctx context.Context, fqn string) ([]string, erro } // Load returns a given instance. -func (r *ReplicaSet) Load(f Factory, path string) (*appsv1.ReplicaSet, error) { - o, err := f.Get("apps/v1/replicasets", path, true, labels.Everything()) +func (*ReplicaSet) Load(f Factory, path string) (*appsv1.ReplicaSet, error) { + o, err := f.Get(client.RsGVR, path, true, labels.Everything()) if err != nil { return nil, err } @@ -69,7 +69,7 @@ func getRSRevision(rs *appsv1.ReplicaSet) (int64, error) { return int64(vers), nil } -func controllerInfo(rs *appsv1.ReplicaSet) (string, string, string, error) { +func controllerInfo(rs *appsv1.ReplicaSet) (name, kind, group string, err error) { for _, ref := range rs.OwnerReferences { if ref.Controller == nil { continue @@ -80,6 +80,7 @@ func controllerInfo(rs *appsv1.ReplicaSet) (string, string, string, error) { } return ref.Name, ref.Kind, group, nil } + return "", "", "", fmt.Errorf("unable to find controller for replicaset: %s", rs.Name) } @@ -114,7 +115,7 @@ func (r *ReplicaSet) Rollback(fqn string) error { } var ddp Deployment - ddp.Init(r.Factory, client.NewGVR("apps/v1/deployments")) + ddp.Init(r.Factory, client.DpGVR) dp, err := ddp.GetInstance(client.FQN(rs.Namespace, name)) if err != nil { return err diff --git a/internal/dao/screen_dump.go b/internal/dao/screen_dump.go index 18b304d3..ea5013d5 100644 --- a/internal/dao/screen_dump.go +++ b/internal/dao/screen_dump.go @@ -25,12 +25,12 @@ type ScreenDump struct { } // Delete a ScreenDump. -func (d *ScreenDump) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error { +func (*ScreenDump) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error { return os.Remove(path) } // List returns a collection of screen dumps. -func (d *ScreenDump) List(ctx context.Context, _ string) ([]runtime.Object, error) { +func (*ScreenDump) List(ctx context.Context, _ string) ([]runtime.Object, error) { dir, ok := ctx.Value(internal.KeyDir).(string) if !ok { return nil, errors.New("no screendump dir found in context") diff --git a/internal/dao/secret.go b/internal/dao/secret.go index 986746fb..298b804b 100644 --- a/internal/dao/secret.go +++ b/internal/dao/secret.go @@ -40,7 +40,7 @@ func (s *Secret) SetDecodeData(b bool) { // Decode removes the encoded part from the secret's description and appends the // secret's decoded data. func (s *Secret) Decode(encodedDescription, path string) (string, error) { - o, err := s.getFactory().Get(s.GVR(), path, true, labels.Everything()) + o, err := s.getFactory().Get(s.gvr, path, true, labels.Everything()) if err != nil { return "", err } @@ -71,7 +71,6 @@ func (s *Secret) Decode(encodedDescription, path string) (string, error) { } return body + "\n" + strings.Join(decodedSecrets, "\n"), nil - } // ExtractSecrets takes an unstructured object and attempts to convert it into a diff --git a/internal/dao/secret_test.go b/internal/dao/secret_test.go index 28240210..be791697 100644 --- a/internal/dao/secret_test.go +++ b/internal/dao/secret_test.go @@ -9,11 +9,12 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestEncodedSecretDescribe(t *testing.T) { var s dao.Secret - s.Init(makeFactory(), client.NewGVR("v1/secrets")) + s.Init(makeFactory(), client.SecGVR) encodedString := ` Name: bootstrap-token-abcdef @@ -39,6 +40,6 @@ token-secret: 24 bytes` "token-secret: 0123456789abcdef" decodedDescription, err := s.Decode(encodedString, "kube-system/bootstrap-token-abcdef") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, expected, decodedDescription) } diff --git a/internal/dao/sts.go b/internal/dao/sts.go index 153d8296..c6f7d9f0 100644 --- a/internal/dao/sts.go +++ b/internal/dao/sts.go @@ -20,9 +20,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 ( @@ -42,7 +39,7 @@ type StatefulSet struct { } // ListImages lists container images. -func (s *StatefulSet) ListImages(ctx context.Context, fqn string) ([]string, error) { +func (s *StatefulSet) ListImages(_ context.Context, fqn string) ([]string, error) { sts, err := s.GetInstance(s.Factory, fqn) if err != nil { return nil, err @@ -53,86 +50,17 @@ func (s *StatefulSet) ListImages(ctx context.Context, fqn string) ([]string, err // Scale a StatefulSet. func (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) error { - ns, n := client.Namespaced(path) - auth, err := s.Client().CanI(ns, "apps/v1/statefulsets:scale", n, []string{client.GetVerb, client.UpdateVerb}) - if err != nil { - return err - } - if !auth { - return fmt.Errorf("user is not authorized to scale statefulsets") - } - - dial, err := s.Client().Dial() - if err != nil { - return err - } - scale, err := dial.AppsV1().StatefulSets(ns).GetScale(ctx, n, metav1.GetOptions{}) - if err != nil { - return err - } - scale.Spec.Replicas = replicas - _, err = dial.AppsV1().StatefulSets(ns).UpdateScale(ctx, n, scale, metav1.UpdateOptions{}) - - return err + return scaleRes(ctx, s.getFactory(), client.StsGVR, path, replicas) } // Restart a StatefulSet rollout. func (s *StatefulSet) Restart(ctx context.Context, path string) error { - sts, err := s.GetInstance(s.Factory, path) - if err != nil { - return err - } - - ns, n := client.Namespaced(path) - pp, err := podsFromSelector(s.Factory, ns, sts.Spec.Selector.MatchLabels) - if err != nil { - return err - } - for _, p := range pp { - s.Forwarders().Kill(client.FQN(p.Namespace, p.Name)) - } - - auth, err := s.Client().CanI(sts.Namespace, "apps/v1/statefulsets", n, client.PatchAccess) - if err != nil { - return err - } - if !auth { - return fmt.Errorf("user is not authorized to restart a statefulset") - } - - dial, err := s.Client().Dial() - if err != nil { - return err - } - - before, err := runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), sts) - if err != nil { - return err - } - - after, err := polymorphichelpers.ObjectRestarterFn(sts) - if err != nil { - return err - } - diff, err := strategicpatch.CreateTwoWayMergePatch(before, after, sts) - if err != nil { - return err - } - _, err = dial.AppsV1().StatefulSets(sts.Namespace).Patch( - ctx, - sts.Name, - types.StrategicMergePatchType, - diff, - metav1.PatchOptions{}, - ) - - return err - + return restartRes[*appsv1.StatefulSet](ctx, s.getFactory(), client.StsGVR, path) } // GetInstance returns a statefulset instance. func (*StatefulSet) GetInstance(f Factory, fqn string) (*appsv1.StatefulSet, error) { - o, err := f.Get("apps/v1/statefulsets", fqn, true, labels.Everything()) + o, err := f.Get(client.StsGVR, fqn, true, labels.Everything()) if err != nil { return nil, err } @@ -170,7 +98,7 @@ func (s *StatefulSet) Pod(fqn string) (string, error) { } func (s *StatefulSet) getStatefulSet(fqn string) (*appsv1.StatefulSet, error) { - o, err := s.getFactory().Get(s.gvrStr(), fqn, true, labels.Everything()) + o, err := s.getFactory().Get(s.gvr, fqn, true, labels.Everything()) if err != nil { return nil, err } @@ -185,9 +113,9 @@ func (s *StatefulSet) getStatefulSet(fqn string) (*appsv1.StatefulSet, error) { } // ScanSA scans for serviceaccount refs. -func (s *StatefulSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { +func (s *StatefulSet) ScanSA(_ context.Context, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := s.getFactory().List(s.GVR(), ns, wait, labels.Everything()) + oo, err := s.getFactory().List(s.gvr, ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -211,9 +139,9 @@ func (s *StatefulSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, } // Scan scans for cluster resource refs. -func (s *StatefulSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) { +func (s *StatefulSet) Scan(_ context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := s.getFactory().List(s.GVR(), ns, wait, labels.Everything()) + oo, err := s.getFactory().List(s.gvr, ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -226,7 +154,7 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait return nil, errors.New("expecting StatefulSet resource") } switch gvr { - case CmGVR: + case client.CmGVR: if !hasConfigMap(&sts.Spec.Template.Spec, n) { continue } @@ -234,7 +162,7 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait GVR: s.GVR(), FQN: client.FQN(sts.Namespace, sts.Name), }) - case SecGVR: + case client.SecGVR: found, err := hasSecret(s.Factory, &sts.Spec.Template.Spec, sts.Namespace, n, wait) if err != nil { slog.Warn("Locate secret failed", @@ -250,9 +178,9 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait GVR: s.GVR(), FQN: client.FQN(sts.Namespace, sts.Name), }) - case PvcGVR: - for _, v := range sts.Spec.VolumeClaimTemplates { - if !strings.HasPrefix(n, v.Name+"-"+sts.Name) { + case client.PvcGVR: + for i := range sts.Spec.VolumeClaimTemplates { + if !strings.HasPrefix(n, sts.Spec.VolumeClaimTemplates[i].Name+"-"+sts.Name) { continue } refs = append(refs, Ref{ @@ -267,7 +195,7 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait GVR: s.GVR(), FQN: client.FQN(sts.Namespace, sts.Name), }) - case PcGVR: + case client.PcGVR: if !hasPC(&sts.Spec.Template.Spec, n) { continue } @@ -275,7 +203,6 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait GVR: s.GVR(), FQN: client.FQN(sts.Namespace, sts.Name), }) - } } @@ -295,7 +222,7 @@ func (s *StatefulSet) GetPodSpec(path string) (*v1.PodSpec, error) { // SetImages sets container images. func (s *StatefulSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error { ns, n := client.Namespaced(path) - auth, err := s.Client().CanI(ns, "apps/v1/statefulset", n, client.PatchAccess) + auth, err := s.Client().CanI(ns, client.StsGVR, n, client.PatchAccess) if err != nil { return err } @@ -319,26 +246,3 @@ func (s *StatefulSet) SetImages(ctx context.Context, path string, imageSpecs Ima ) return err } - -func podsFromSelector(f Factory, ns string, sel map[string]string) ([]*v1.Pod, error) { - oo, err := f.List("v1/pods", ns, true, labels.Set(sel).AsSelector()) - if err != nil { - return nil, err - } - - if len(oo) == 0 { - return nil, fmt.Errorf("no matching pods for %v", sel) - } - - pp := make([]*v1.Pod, 0, len(oo)) - for _, o := range oo { - pod := new(v1.Pod) - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, pod) - if err != nil { - return nil, err - } - pp = append(pp, pod) - } - - return pp, nil -} diff --git a/internal/dao/svc.go b/internal/dao/svc.go index cee13178..64d84e2c 100644 --- a/internal/dao/svc.go +++ b/internal/dao/svc.go @@ -51,7 +51,7 @@ func (s *Service) Pod(fqn string) (string, error) { // GetInstance returns a service instance. func (s *Service) GetInstance(fqn string) (*v1.Service, error) { - o, err := s.getFactory().Get(s.gvrStr(), fqn, true, labels.Everything()) + o, err := s.getFactory().Get(s.gvr, fqn, true, labels.Everything()) if err != nil { return nil, err } @@ -69,7 +69,7 @@ func (s *Service) GetInstance(fqn string) (*v1.Service, error) { // Helpers... func podFromSelector(f Factory, ns string, sel map[string]string) (string, error) { - oo, err := f.List("v1/pods", ns, true, labels.Set(sel).AsSelector()) + oo, err := f.List(client.PodGVR, ns, true, labels.Set(sel).AsSelector()) if err != nil { return "", err } diff --git a/internal/dao/table.go b/internal/dao/table.go index 362ce0c2..0952dc89 100644 --- a/internal/dao/table.go +++ b/internal/dao/table.go @@ -6,16 +6,27 @@ package dao import ( "context" "fmt" + "log/slog" + "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/slogs" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/rest" ) -const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json" +const ( + gvFmt = "application/json;as=Table;v=%s;g=%s, application/json" + includeMeta = "Metadata" + includeObj = "Object" + includeNone = "None" + header = "application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json" +) var genScheme = runtime.NewScheme() @@ -51,33 +62,85 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { labelSel, _ := ctx.Value(internal.KeyLabels).(string) fieldSel, _ := ctx.Value(internal.KeyFields).(string) - f, p := t.codec() + includeObject := includeMeta + if t.includeObj { + includeObject = includeObj + } + + f, _ := t.codec() c, err := t.getClient(f) if err != nil { return nil, err } - a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName) + ti := time.Now() o, err := c.Get(). - SetHeader("Accept", a). + SetHeader("Accept", header). + Param("includeObject", includeObject). Namespace(ns). Resource(t.gvr.R()). VersionedParams(&metav1.ListOptions{ - LabelSelector: labelSel, - FieldSelector: fieldSel, - ResourceVersion: "0", - ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan, - }, p). + LabelSelector: labelSel, + FieldSelector: fieldSel, + }, metav1.ParameterCodec). Do(ctx).Get() if err != nil { return nil, err } + slog.Debug("Q Time", slogs.Elapsed, time.Since(ti)) - return []runtime.Object{o}, nil + namespaced := true + if res, e := MetaAccess.MetaFor(t.gvr); e == nil && !res.Namespaced { + namespaced = false + } + ta, err := decodeTable(ctx, o.(*metav1.Table), namespaced) + if err != nil { + return nil, err + } + + return []runtime.Object{ta}, nil } // ---------------------------------------------------------------------------- // Helpers... +func decodeTable(ctx context.Context, table *metav1.Table, namespaced bool) (runtime.Object, error) { + if namespaced { + table.ColumnDefinitions = append([]metav1.TableColumnDefinition{{Name: "Namespace", Type: "string"}}, table.ColumnDefinitions...) + } + pool := internal.NewWorkerPool(ctx, internal.DefaultPoolSize) + for i := range table.Rows { + pool.Add(func(_ context.Context) error { + row := &table.Rows[i] + if row.Object.Raw == nil || row.Object.Object != nil { + return nil + } + converted, err := runtime.Decode(unstructured.UnstructuredJSONScheme, row.Object.Raw) + if err != nil { + return err + } + row.Object.Object = converted + var m metav1.Object + if obj := row.Object.Object; obj != nil { + m, _ = meta.Accessor(obj) + } + var ns string + if m != nil { + ns = m.GetNamespace() + } + if namespaced { + row.Cells = append([]any{ns}, row.Cells...) + } + return nil + }) + } + errs := pool.Drain() + if len(errs) > 0 { + return nil, fmt.Errorf("failed to decode table rows: %w", errs[0]) + } + + return table, nil +} + func (t *Table) getClient(f serializer.CodecFactory) (*rest.RESTClient, error) { cfg, err := t.Client().RestConfig() if err != nil { @@ -90,7 +153,6 @@ func (t *Table) getClient(f serializer.CodecFactory) (*rest.RESTClient, error) { cfg.APIPath = "/api" } cfg.NegotiatedSerializer = f.WithoutConversion() - crRestClient, err := rest.RESTClientFor(cfg) if err != nil { return nil, err diff --git a/internal/dao/types.go b/internal/dao/types.go index fa7d833b..08f1d906 100644 --- a/internal/dao/types.go +++ b/internal/dao/types.go @@ -18,28 +18,22 @@ import ( restclient "k8s.io/client-go/rest" ) -// ResourceMetas represents a collection of resource metadata. -type ResourceMetas map[client.GVR]metav1.APIResource - -// Accessors represents a collection of dao accessors. -type Accessors map[client.GVR]Accessor - // Factory represents a resource factory. type Factory interface { // Client retrieves an api client. Client() client.Connection // Get fetch a given resource. - Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) + Get(gvr *client.GVR, path string, wait bool, sel labels.Selector) (runtime.Object, error) // List fetch a collection of resources. - List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) + List(gvr *client.GVR, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) // ForResource fetch an informer for a given resource. - ForResource(ns, gvr string) (informers.GenericInformer, error) + ForResource(ns string, gvr *client.GVR) (informers.GenericInformer, error) // CanForResource fetch an informer for a given resource if authorized - CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) + CanForResource(ns string, gvr *client.GVR, verbs []string) (informers.GenericInformer, error) // WaitForCacheSync synchronize the cache. WaitForCacheSync() @@ -75,7 +69,7 @@ type Accessor interface { Getter // Init the resource with a factory object. - Init(Factory, client.GVR) + Init(Factory, *client.GVR) // GVR returns a gvr a string. GVR() string diff --git a/internal/dao/utils_test.go b/internal/dao/utils_test.go index 1984f63a..e98db88b 100644 --- a/internal/dao/utils_test.go +++ b/internal/dao/utils_test.go @@ -20,14 +20,14 @@ import ( ) type testFactory struct { - inventory map[string]map[string][]runtime.Object + inventory map[string]map[*client.GVR][]runtime.Object } func makeFactory() dao.Factory { return &testFactory{ - inventory: map[string]map[string][]runtime.Object{ + inventory: map[string]map[*client.GVR][]runtime.Object{ "kube-system": { - "v1/secrets": { + client.SecGVR: { load("secret"), }, }, @@ -37,10 +37,10 @@ func makeFactory() dao.Factory { var _ dao.Factory = &testFactory{} -func (f *testFactory) Client() client.Connection { +func (*testFactory) Client() client.Connection { return nil } -func (f *testFactory) Get(gvr, fqn string, wait bool, sel labels.Selector) (runtime.Object, error) { +func (f *testFactory) Get(gvr *client.GVR, fqn string, _ bool, _ labels.Selector) (runtime.Object, error) { ns, po := path.Split(fqn) ns = strings.Trim(ns, "/") @@ -52,21 +52,21 @@ func (f *testFactory) Get(gvr, fqn string, wait bool, sel labels.Selector) (runt return nil, nil } -func (f *testFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) { +func (f *testFactory) List(gvr *client.GVR, ns string, _ bool, _ labels.Selector) ([]runtime.Object, error) { return f.inventory[ns][gvr], nil } -func (f *testFactory) ForResource(ns, gvr string) (informers.GenericInformer, error) { +func (*testFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) { return nil, nil } -func (f *testFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { +func (*testFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) { return nil, nil } -func (f *testFactory) WaitForCacheSync() {} -func (f *testFactory) Forwarders() watch.Forwarders { +func (*testFactory) WaitForCacheSync() {} +func (*testFactory) Forwarders() watch.Forwarders { return nil } -func (f *testFactory) DeleteForwarder(string) {} +func (*testFactory) DeleteForwarder(string) {} func load(n string) *unstructured.Unstructured { raw, _ := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) diff --git a/internal/dao/workload.go b/internal/dao/workload.go index 9d91aa87..37086c0d 100644 --- a/internal/dao/workload.go +++ b/internal/dao/workload.go @@ -26,20 +26,14 @@ const ( DegradedStatus = "DEGRADED" ) -var ( - SaGVR = client.NewGVR("v1/serviceaccounts") - PvcGVR = client.NewGVR("v1/persistentvolumeclaims") - PcGVR = client.NewGVR("scheduling.k8s.io/v1/priorityclasses") - CmGVR = client.NewGVR("v1/configmaps") - SecGVR = client.NewGVR("v1/secrets") - PodGVR = client.NewGVR("v1/pods") - SvcGVR = client.NewGVR("v1/services") - DsGVR = client.NewGVR("apps/v1/daemonsets") - StsGVR = client.NewGVR("apps/v1/statefulSets") - DpGVR = client.NewGVR("apps/v1/deployments") - RsGVR = client.NewGVR("apps/v1/replicasets") - resList = []client.GVR{PodGVR, SvcGVR, DsGVR, StsGVR, DpGVR, RsGVR} -) +var resList = []*client.GVR{ + client.PodGVR, + client.SvcGVR, + client.DsGVR, + client.StsGVR, + client.DpGVR, + client.RsGVR, +} // Workload tracks a select set of resources in a given namespace. type Workload struct { @@ -47,9 +41,9 @@ type Workload struct { } func (w *Workload) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error { - gvr, _ := ctx.Value(internal.KeyGVR).(client.GVR) + gvr, _ := ctx.Value(internal.KeyGVR).(*client.GVR) ns, n := client.Namespaced(path) - auth, err := w.Client().CanI(ns, gvr.String(), n, []string{client.DeleteVerb}) + auth, err := w.Client().CanI(ns, gvr, n, []string{client.DeleteVerb}) if err != nil { return err } @@ -81,7 +75,7 @@ func (w *Workload) Delete(ctx context.Context, path string, propagation *metav1. return dial.Namespace(ns).Delete(ctx, n, opts) } -func (a *Workload) fetch(ctx context.Context, gvr client.GVR, ns string) (*metav1.Table, error) { +func (a *Workload) fetch(ctx context.Context, gvr *client.GVR, ns string) (*metav1.Table, error) { a.gvr = gvr oo, err := a.Table.List(ctx, ns) if err != nil { @@ -121,13 +115,13 @@ func (a *Workload) List(ctx context.Context, ns string) ([]runtime.Object, error ns, ts = m.GetNamespace(), m.CreationTimestamp } } - stat := status(gvr, r, table.ColumnDefinitions) - oo = append(oo, &render.WorkloadRes{Row: metav1.TableRow{Cells: []interface{}{ + stat := status(gvr, &r, table.ColumnDefinitions) + oo = append(oo, &render.WorkloadRes{Row: metav1.TableRow{Cells: []any{ gvr.String(), ns, r.Cells[indexOf("Name", table.ColumnDefinitions)], stat, - readiness(gvr, r, table.ColumnDefinitions), + readiness(gvr, &r, table.ColumnDefinitions), validity(stat), ts, }}}) @@ -139,34 +133,34 @@ func (a *Workload) List(ctx context.Context, ns string) ([]runtime.Object, error // Helpers... -func readiness(gvr client.GVR, r metav1.TableRow, h []metav1.TableColumnDefinition) string { +func readiness(gvr *client.GVR, r *metav1.TableRow, h []metav1.TableColumnDefinition) string { switch gvr { - case PodGVR, DpGVR, StsGVR: + case client.PodGVR, client.DpGVR, client.StsGVR: return r.Cells[indexOf("Ready", h)].(string) - case RsGVR, DsGVR: + case client.RsGVR, client.DsGVR: c := r.Cells[indexOf("Ready", h)].(int64) d := r.Cells[indexOf("Desired", h)].(int64) return fmt.Sprintf("%d/%d", c, d) - case SvcGVR: + case client.SvcGVR: return "" } return render.NAValue } -func status(gvr client.GVR, r metav1.TableRow, h []metav1.TableColumnDefinition) string { +func status(gvr *client.GVR, r *metav1.TableRow, h []metav1.TableColumnDefinition) string { switch gvr { - case PodGVR: + case client.PodGVR: if status := r.Cells[indexOf("Status", h)]; status == render.PhaseCompleted { return StatusOK } else if !isReady(r.Cells[indexOf("Ready", h)].(string)) || status != render.PhaseRunning { return DegradedStatus } - case DpGVR, StsGVR: + case client.DpGVR, client.StsGVR: if !isReady(r.Cells[indexOf("Ready", h)].(string)) { return DegradedStatus } - case RsGVR, DsGVR: + case client.RsGVR, client.DsGVR: rd, ok1 := r.Cells[indexOf("Ready", h)].(int64) de, ok2 := r.Cells[indexOf("Desired", h)].(int64) if ok1 && ok2 { @@ -182,7 +176,7 @@ func status(gvr client.GVR, r metav1.TableRow, h []metav1.TableColumnDefinition) return DegradedStatus } } - case SvcGVR: + case client.SvcGVR: default: return render.MissingValue } diff --git a/internal/health/check.go b/internal/health/check.go index 745c6e2a..052db7ba 100644 --- a/internal/health/check.go +++ b/internal/health/check.go @@ -4,6 +4,7 @@ package health import ( + "github.com/derailed/k9s/internal/client" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -12,14 +13,14 @@ import ( type Check struct { Counts - GVR string + GVR *client.GVR } // Checks represents a collection of health checks. type Checks []*Check // NewCheck returns a new health check. -func NewCheck(gvr string) *Check { +func NewCheck(gvr *client.GVR) *Check { return &Check{ GVR: gvr, Counts: make(Counts), diff --git a/internal/health/check_test.go b/internal/health/check_test.go index 1b6f1126..3f688c6f 100644 --- a/internal/health/check_test.go +++ b/internal/health/check_test.go @@ -6,6 +6,7 @@ package health_test import ( "testing" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/health" "github.com/stretchr/testify/assert" ) @@ -13,16 +14,16 @@ import ( func TestCheck(t *testing.T) { var cc health.Checks - c := health.NewCheck("test") + c := health.NewCheck(client.NewGVR("test")) n := 0 - for i := 0; i < 10; i++ { + for range 10 { c.Inc(health.S1) cc = append(cc, c) n++ } c.Total(int64(n)) - assert.Equal(t, 10, len(cc)) + assert.Len(t, cc, 10) assert.Equal(t, int64(10), c.Tally(health.Corpus)) assert.Equal(t, int64(10), c.Tally(health.S1)) assert.Equal(t, int64(0), c.Tally(health.S2)) diff --git a/internal/helpers.go b/internal/helpers.go index aa1d43e5..66fe9f01 100644 --- a/internal/helpers.go +++ b/internal/helpers.go @@ -11,9 +11,8 @@ import ( ) var ( - inverseRx = regexp.MustCompile(`\A\!`) - fuzzyRx = regexp.MustCompile(`\A-f\s?([\w-]+)\b`) - labelRx = regexp.MustCompile(`\A\-l`) + fuzzyRx = regexp.MustCompile(`\A-f\s?([\w-]+)\b`) + labelRx = regexp.MustCompile(`\A\-l`) ) // Helpers... @@ -23,7 +22,7 @@ func IsInverseSelector(s string) bool { if s == "" { return false } - return inverseRx.MatchString(s) + return s[0] == '!' } // IsLabelSelector checks if query is a label query. diff --git a/internal/model/cluster_info.go b/internal/model/cluster_info.go index 689197a2..bc2c7180 100644 --- a/internal/model/cluster_info.go +++ b/internal/model/cluster_info.go @@ -30,10 +30,10 @@ const ( // ClusterInfoListener registers a listener for model changes. type ClusterInfoListener interface { // ClusterInfoChanged notifies the cluster meta was changed. - ClusterInfoChanged(prev, curr ClusterMeta) + ClusterInfoChanged(prev, curr *ClusterMeta) // ClusterInfoUpdated notifies the cluster meta was updated. - ClusterInfoUpdated(ClusterMeta) + ClusterInfoUpdated(*ClusterMeta) } // ClusterMeta represents cluster meta data. @@ -46,8 +46,8 @@ type ClusterMeta struct { } // NewClusterMeta returns a new instance. -func NewClusterMeta() ClusterMeta { - return ClusterMeta{ +func NewClusterMeta() *ClusterMeta { + return &ClusterMeta{ Context: client.NA, Cluster: client.NA, User: client.NA, @@ -60,7 +60,7 @@ func NewClusterMeta() ClusterMeta { } // Deltas diffs cluster meta return true if different, false otherwise. -func (c ClusterMeta) Deltas(n ClusterMeta) bool { +func (c *ClusterMeta) Deltas(n *ClusterMeta) bool { if c.Cpu != n.Cpu || c.Mem != n.Mem || c.Ephemeral != n.Ephemeral { return true } @@ -77,7 +77,7 @@ func (c ClusterMeta) Deltas(n ClusterMeta) bool { type ClusterInfo struct { cluster *Cluster factory dao.Factory - data ClusterMeta + data *ClusterMeta version string cfg *config.K9s listeners []ClusterInfoListener @@ -122,9 +122,7 @@ func (c *ClusterInfo) Reset(f dao.Factory) { } c.mx.Lock() - { - c.cluster, c.data = NewCluster(f), NewClusterMeta() - } + c.cluster, c.data = NewCluster(f), NewClusterMeta() c.mx.Unlock() c.Refresh() @@ -165,9 +163,7 @@ func (c *ClusterInfo) Refresh() { c.fireNoMetaChanged(data) } c.mx.Lock() - { - c.data = data - } + c.data = data c.mx.Unlock() } @@ -191,13 +187,13 @@ func (c *ClusterInfo) RemoveListener(l ClusterInfoListener) { } } -func (c *ClusterInfo) fireMetaChanged(prev, cur ClusterMeta) { +func (c *ClusterInfo) fireMetaChanged(prev, cur *ClusterMeta) { for _, l := range c.listeners { l.ClusterInfoChanged(prev, cur) } } -func (c *ClusterInfo) fireNoMetaChanged(data ClusterMeta) { +func (c *ClusterInfo) fireNoMetaChanged(data *ClusterMeta) { for _, l := range c.listeners { l.ClusterInfoUpdated(data) } @@ -210,7 +206,7 @@ func fetchLatestRev() (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, k9sGitURL, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, k9sGitURL, http.NoBody) if err != nil { return "", err } @@ -228,7 +224,7 @@ func fetchLatestRev() (string, error) { if err != nil { return "", err } - m := make(map[string]interface{}, 20) + m := make(map[string]any, 20) if err := json.Unmarshal(b, &m); err != nil { return "", err } diff --git a/internal/model/cluster_info_test.go b/internal/model/cluster_info_test.go index ca0c637e..95c320d0 100644 --- a/internal/model/cluster_info_test.go +++ b/internal/model/cluster_info_test.go @@ -17,7 +17,7 @@ func init() { func TestClusterMetaDelta(t *testing.T) { uu := map[string]struct { - o, n model.ClusterMeta + o, n *model.ClusterMeta e bool }{ "empty": { @@ -45,7 +45,7 @@ func TestClusterMetaDelta(t *testing.T) { // Helpers... -func makeClusterMeta(cluster string) model.ClusterMeta { +func makeClusterMeta(cluster string) *model.ClusterMeta { m := model.NewClusterMeta() m.Cluster = cluster m.Cpu, m.Mem = 10, 20 diff --git a/internal/model/cmd_buff.go b/internal/model/cmd_buff.go index f62ca799..315526bb 100644 --- a/internal/model/cmd_buff.go +++ b/internal/model/cmd_buff.go @@ -82,9 +82,7 @@ func (c *CmdBuff) IsActive() bool { // SetActive toggles cmd buffer active state. func (c *CmdBuff) SetActive(b bool) { c.mx.Lock() - { - c.active = b - } + c.active = b c.mx.Unlock() c.fireActive(c.active) @@ -123,26 +121,20 @@ func (c *CmdBuff) hasCancel() bool { func (c *CmdBuff) setCancel(f context.CancelFunc) { c.mx.Lock() - { - c.cancel = f - } + c.cancel = f c.mx.Unlock() } func (c *CmdBuff) resetCancel() { c.mx.Lock() - { - c.cancel = nil - } + c.cancel = nil c.mx.Unlock() } // SetText initializes the buffer with a command. func (c *CmdBuff) SetText(text, suggestion string) { c.mx.Lock() - { - c.buff, c.suggestion = []rune(text), suggestion - } + c.buff, c.suggestion = []rune(text), suggestion c.mx.Unlock() c.fireBufferCompleted(c.GetText(), c.GetSuggestion()) } @@ -150,9 +142,7 @@ func (c *CmdBuff) SetText(text, suggestion string) { // Add adds a new character to the buffer. func (c *CmdBuff) Add(r rune) { c.mx.Lock() - { - c.buff = append(c.buff, r) - } + c.buff = append(c.buff, r) c.mx.Unlock() c.fireBufferChanged(c.GetText(), c.GetSuggestion()) if c.hasCancel() { @@ -192,9 +182,7 @@ func (c *CmdBuff) Delete() { // ClearText clears out command buffer. func (c *CmdBuff) ClearText(fire bool) { c.mx.Lock() - { - c.buff, c.suggestion = c.buff[:0], "" - } + c.buff, c.suggestion = c.buff[:0], "" c.mx.Unlock() if fire { @@ -223,9 +211,7 @@ func (c *CmdBuff) Empty() bool { // AddListener registers a cmd buffer listener. func (c *CmdBuff) AddListener(w BuffWatcher) { c.mx.Lock() - { - c.listeners[w] = struct{}{} - } + c.listeners[w] = struct{}{} c.mx.Unlock() } diff --git a/internal/model/cmd_buff_test.go b/internal/model/cmd_buff_test.go index 924470ad..00606425 100644 --- a/internal/model/cmd_buff_test.go +++ b/internal/model/cmd_buff_test.go @@ -65,22 +65,22 @@ func TestCmdBuffChanged(t *testing.T) { b.Delete() assert.Equal(t, 0, l.act) assert.Equal(t, 0, l.inact) - assert.Equal(t, "", l.text) - assert.Equal(t, "", b.GetText()) + assert.Empty(t, l.text) + assert.Empty(t, b.GetText()) b.Add('c') b.ClearText(true) assert.Equal(t, 0, l.act) assert.Equal(t, 0, l.inact) - assert.Equal(t, "", l.text) - assert.Equal(t, "", b.GetText()) + assert.Empty(t, l.text) + assert.Empty(t, b.GetText()) b.Add('c') b.Reset() assert.Equal(t, 0, l.act) assert.Equal(t, 1, l.inact) - assert.Equal(t, "", l.text) - assert.Equal(t, "", b.GetText()) + assert.Empty(t, l.text) + assert.Empty(t, b.GetText()) assert.True(t, b.Empty()) } diff --git a/internal/model/describe.go b/internal/model/describe.go index aab755b0..20edb19c 100644 --- a/internal/model/describe.go +++ b/internal/model/describe.go @@ -22,7 +22,7 @@ import ( // Describe tracks describable resources. type Describe struct { - gvr client.GVR + gvr *client.GVR inUpdate int32 path string query string @@ -33,7 +33,7 @@ type Describe struct { } // NewDescribe returns a new describe resource model. -func NewDescribe(gvr client.GVR, path string) *Describe { +func NewDescribe(gvr *client.GVR, path string) *Describe { return &Describe{ gvr: gvr, path: path, @@ -42,7 +42,7 @@ func NewDescribe(gvr client.GVR, path string) *Describe { } // GVR returns the resource gvr. -func (d *Describe) GVR() client.GVR { +func (d *Describe) GVR() *client.GVR { return d.gvr } @@ -52,7 +52,7 @@ func (d *Describe) GetPath() string { } // SetOptions toggle model options. -func (d *Describe) SetOptions(context.Context, ViewerToggleOpts) {} +func (*Describe) SetOptions(context.Context, ViewerToggleOpts) {} // Filter filters the model. func (d *Describe) Filter(q string) { @@ -91,7 +91,7 @@ func (d *Describe) fireResourceFailed(err error) { } // ClearFilter clear out the filter. -func (d *Describe) ClearFilter() { +func (*Describe) ClearFilter() { } // Peek returns current model state. @@ -172,11 +172,7 @@ func (d *Describe) reconcile(ctx context.Context) error { } // Describe describes a given resource. -func (d *Describe) describe(ctx context.Context, gvr client.GVR, path string) (string, error) { - defer func(t time.Time) { - slog.Debug("Describe model elapsed", slogs.Elapsed, time.Since(t)) - }(time.Now()) - +func (d *Describe) describe(ctx context.Context, gvr *client.GVR, path string) (string, error) { meta, err := getMeta(ctx, gvr) if err != nil { return "", err diff --git a/internal/model/fish_buff.go b/internal/model/fish_buff.go index 1bfb0ae0..ff2233cd 100644 --- a/internal/model/fish_buff.go +++ b/internal/model/fish_buff.go @@ -89,7 +89,7 @@ func (f *FishBuff) CurrentSuggestion() (string, bool) { } // AutoSuggests returns true if model implements auto suggestions. -func (f *FishBuff) AutoSuggests() bool { +func (*FishBuff) AutoSuggests() bool { return true } @@ -107,7 +107,7 @@ func (f *FishBuff) SetSuggestionFn(fn SuggestionFunc) { } // Notify publish suggestions to all listeners. -func (f *FishBuff) Notify(delete bool) { +func (f *FishBuff) Notify(_ bool) { if f.suggestionFn == nil { return } diff --git a/internal/model/fish_buff_test.go b/internal/model/fish_buff_test.go index ae8abe3c..b51ff49f 100644 --- a/internal/model/fish_buff_test.go +++ b/internal/model/fish_buff_test.go @@ -16,7 +16,7 @@ func TestFishAdd(t *testing.T) { f := model.NewFishBuff(' ', model.FilterBuffer) f.AddListener(&m) - f.SetSuggestionFn(func(text string) sort.StringSlice { + f.SetSuggestionFn(func(string) sort.StringSlice { return sort.StringSlice{"blee", "brew"} }) f.Add('b') @@ -45,7 +45,7 @@ func TestFishDelete(t *testing.T) { f := model.NewFishBuff(' ', model.FilterBuffer) f.AddListener(&m) - f.SetSuggestionFn(func(text string) sort.StringSlice { + f.SetSuggestionFn(func(string) sort.StringSlice { return sort.StringSlice{"blee", "duh"} }) f.Add('a') @@ -89,11 +89,11 @@ func (m *mockSuggestionListener) BufferCompleted(text, suggest string) { m.text, m.suggestion = text, suggest } -func (m *mockSuggestionListener) BufferActive(state bool, kind model.BufferKind) { +func (m *mockSuggestionListener) BufferActive(state bool, _ model.BufferKind) { m.active = state } -func (m *mockSuggestionListener) SuggestionChanged(text, sugg string) { +func (m *mockSuggestionListener) SuggestionChanged(_, sugg string) { m.suggestion = sugg m.suggCount++ } diff --git a/internal/model/flash.go b/internal/model/flash.go index 727af7f2..25150b00 100644 --- a/internal/model/flash.go +++ b/internal/model/flash.go @@ -81,7 +81,7 @@ func (f *Flash) Info(msg string) { } // Infof displays a formatted info flash message. -func (f *Flash) Infof(fmat string, args ...interface{}) { +func (f *Flash) Infof(fmat string, args ...any) { f.Info(fmt.Sprintf(fmat, args...)) } @@ -92,7 +92,7 @@ func (f *Flash) Warn(msg string) { } // Warnf displays a formatted warning flash message. -func (f *Flash) Warnf(fmat string, args ...interface{}) { +func (f *Flash) Warnf(fmat string, args ...any) { f.Warn(fmt.Sprintf(fmat, args...)) } @@ -103,11 +103,10 @@ func (f *Flash) Err(err error) { } // Errf displays a formatted error flash message. -func (f *Flash) Errf(fmat string, args ...interface{}) { +func (f *Flash) Errf(fmat string, args ...any) { var err error for _, a := range args { - switch e := a.(type) { - case error: + if e, ok := a.(error); ok { err = e } } diff --git a/internal/model/flash_test.go b/internal/model/flash_test.go index 2484fdef..d2f5a96d 100644 --- a/internal/model/flash_test.go +++ b/internal/model/flash_test.go @@ -80,7 +80,7 @@ func newFlash() *flash { return &flash{} } -func (f *flash) getMetrics() (int, model.FlashLevel, string) { +func (f *flash) getMetrics() (val int, lvl model.FlashLevel, msg string) { return f.set, f.level, f.msg } diff --git a/internal/model/helpers.go b/internal/model/helpers.go index 3d7efbcf..a2354eb6 100644 --- a/internal/model/helpers.go +++ b/internal/model/helpers.go @@ -18,7 +18,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func getMeta(ctx context.Context, gvr client.GVR) (ResourceMeta, error) { +func getMeta(ctx context.Context, gvr *client.GVR) (ResourceMeta, error) { meta := resourceMeta(gvr) factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory) if !ok { @@ -29,11 +29,11 @@ func getMeta(ctx context.Context, gvr client.GVR) (ResourceMeta, error) { return meta, nil } -func resourceMeta(gvr client.GVR) ResourceMeta { +func resourceMeta(gvr *client.GVR) ResourceMeta { meta, ok := Registry[gvr.String()] if !ok { meta = ResourceMeta{ - DAO: new(dao.Dynamic), + DAO: new(dao.Table), Renderer: new(render.Table), } } @@ -45,7 +45,7 @@ func resourceMeta(gvr client.GVR) ResourceMeta { } // MetaFQN returns a fully qualified resource name. -func MetaFQN(m metav1.ObjectMeta) string { +func MetaFQN(m *metav1.ObjectMeta) string { return FQN(m.Namespace, m.Name) } @@ -58,9 +58,9 @@ func FQN(ns, n string) string { } // NewExpBackOff returns a new exponential backoff timer. -func NewExpBackOff(ctx context.Context, start, max time.Duration) backoff.BackOffContext { +func NewExpBackOff(ctx context.Context, start, maxVal time.Duration) backoff.BackOffContext { bf := backoff.NewExponentialBackOff() - bf.InitialInterval, bf.MaxElapsedTime = start, max + bf.InitialInterval, bf.MaxElapsedTime = start, maxVal return backoff.WithContext(bf, ctx) } diff --git a/internal/model/helpers_test.go b/internal/model/helpers_test.go index 69ed2529..59226b5f 100644 --- a/internal/model/helpers_test.go +++ b/internal/model/helpers_test.go @@ -29,7 +29,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, model.MetaFQN(u.meta)) + assert.Equal(t, u.e, model.MetaFQN(&u.meta)) }) } } diff --git a/internal/model/hint_test.go b/internal/model/hint_test.go index 383b2c50..f0de29ed 100644 --- a/internal/model/hint_test.go +++ b/internal/model/hint_test.go @@ -37,7 +37,7 @@ func TestHint(t *testing.T) { h.SetHints(u.hh) assert.Equal(t, u.e, l.count) - assert.Equal(t, u.e, len(h.Peek())) + assert.Len(t, h.Peek(), u.e) }) } } diff --git a/internal/model/history.go b/internal/model/history.go index 37c4c5de..4d6ce114 100644 --- a/internal/model/history.go +++ b/internal/model/history.go @@ -48,7 +48,7 @@ func (h *History) Back() bool { } h.previousCommandIndex = h.activeCommandIndex - h.activeCommandIndex = h.activeCommandIndex - 1 + h.activeCommandIndex-- return true } @@ -64,7 +64,7 @@ func (h *History) Forward() bool { } h.previousCommandIndex = h.activeCommandIndex - h.activeCommandIndex = h.activeCommandIndex + 1 + h.activeCommandIndex++ return true } diff --git a/internal/model/log.go b/internal/model/log.go index 19495847..c387aa3b 100644 --- a/internal/model/log.go +++ b/internal/model/log.go @@ -44,7 +44,7 @@ type Log struct { factory dao.Factory lines *dao.LogItems listeners []LogsListener - gvr client.GVR + gvr *client.GVR logOptions *dao.LogOptions cancelFn context.CancelFunc mx sync.RWMutex @@ -54,7 +54,7 @@ type Log struct { } // NewLog returns a new model. -func NewLog(gvr client.GVR, opts *dao.LogOptions, flushTimeout time.Duration) *Log { +func NewLog(gvr *client.GVR, opts *dao.LogOptions, flushTimeout time.Duration) *Log { return &Log{ gvr: gvr, logOptions: opts, @@ -63,7 +63,7 @@ func NewLog(gvr client.GVR, opts *dao.LogOptions, flushTimeout time.Duration) *L } } -func (l *Log) GVR() client.GVR { +func (l *Log) GVR() *client.GVR { return l.gvr } @@ -95,9 +95,7 @@ func (l *Log) ToggleShowTimestamp(b bool) { func (l *Log) Head(ctx context.Context) { l.mx.Lock() - { - l.logOptions.Head = true - } + l.logOptions.Head = true l.mx.Unlock() l.Restart(ctx) } @@ -110,7 +108,7 @@ func (l *Log) SetSinceSeconds(ctx context.Context, i int64) { // Configure sets logger configuration. func (l *Log) Configure(opts config.Logger) { - l.logOptions.Lines = int64(opts.TailCount) + l.logOptions.Lines = opts.TailCount l.logOptions.SinceSeconds = opts.SinceSeconds } @@ -137,10 +135,8 @@ func (l *Log) Init(f dao.Factory) { // Clear the logs. func (l *Log) Clear() { l.mx.Lock() - { - l.lines.Clear() - l.lastSent = 0 - } + l.lines.Clear() + l.lastSent = 0 l.mx.Unlock() l.fireLogCleared() @@ -178,9 +174,7 @@ func (l *Log) Stop() { // Set sets the log lines (for testing only!) func (l *Log) Set(lines *dao.LogItems) { l.mx.Lock() - { - l.lines.Merge(lines) - } + l.lines.Merge(lines) l.mx.Unlock() l.fireLogCleared() @@ -192,9 +186,7 @@ func (l *Log) Set(lines *dao.LogItems) { // ClearFilter resets the log filter if any. func (l *Log) ClearFilter() { l.mx.Lock() - { - l.filter = "" - } + l.filter = "" l.mx.Unlock() l.fireLogCleared() @@ -206,9 +198,7 @@ func (l *Log) ClearFilter() { // Filter filters the model using either fuzzy or regexp. func (l *Log) Filter(q string) { l.mx.Lock() - { - l.filter = q - } + l.filter = q l.mx.Unlock() l.fireLogCleared() @@ -303,9 +293,7 @@ func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) { l.Append(item) var overflow bool l.mx.RLock() - { - overflow = int64(l.lines.Len()-l.lastSent) > l.logOptions.Lines - } + overflow = int64(l.lines.Len()-l.lastSent) > l.logOptions.Lines l.mx.RUnlock() if overflow { l.Notify() @@ -418,9 +406,7 @@ func (l *Log) fireLogChanged(lines [][]byte) { func (l *Log) fireLogCleared() { var ll []LogsListener l.mx.RLock() - { - ll = l.listeners - } + ll = l.listeners l.mx.RUnlock() for _, lis := range ll { lis.LogCleared() diff --git a/internal/model/log_int_test.go b/internal/model/log_int_test.go index 57dd9520..2cdf5b86 100644 --- a/internal/model/log_int_test.go +++ b/internal/model/log_int_test.go @@ -27,10 +27,9 @@ func TestUpdateLogs(t *testing.T) { defer cancel() go m.updateLogs(ctx, c) - for i := 0; i < 2*size; i++ { + for i := range 2 * size { c <- dao.NewLogItemFromString("line" + strconv.Itoa(i)) } - time.Sleep(2 * time.Second) assert.Equal(t, size, v.count) } @@ -51,7 +50,7 @@ func BenchmarkUpdateLogs(b *testing.B) { b.ReportAllocs() b.ResetTimer() - for n := 0; n < b.N; n++ { + for range b.N { c <- item } close(c) @@ -78,8 +77,8 @@ func newMockLogView() *mockLogView { func (t *mockLogView) LogChanged(ll [][]byte) { t.count += len(ll) } -func (t *mockLogView) LogStop() {} -func (t *mockLogView) LogCanceled() {} -func (t *mockLogView) LogResume() {} -func (t *mockLogView) LogCleared() {} -func (t *mockLogView) LogFailed(err error) {} +func (*mockLogView) LogStop() {} +func (*mockLogView) LogCanceled() {} +func (*mockLogView) LogResume() {} +func (*mockLogView) LogCleared() {} +func (*mockLogView) LogFailed(error) {} diff --git a/internal/model/log_test.go b/internal/model/log_test.go index b3fd501c..6296d7b8 100644 --- a/internal/model/log_test.go +++ b/internal/model/log_test.go @@ -29,7 +29,7 @@ func TestLogFullBuffer(t *testing.T) { m.AddListener(v) data := dao.NewLogItems() - for i := 0; i < 2*size; i++ { + for i := range 2 * size { data.Add(dao.NewLogItemFromString("line" + strconv.Itoa(i))) m.Append(data.Items()[i]) } @@ -75,7 +75,7 @@ func TestLogFilter(t *testing.T) { m.Filter(u.q) data := dao.NewLogItems() - for i := 0; i < size; i++ { + for i := range size { data.Add(dao.NewLogItemFromString(fmt.Sprintf("pod-line-%d", i+1))) m.Append(data.Items()[i]) } @@ -84,13 +84,13 @@ func TestLogFilter(t *testing.T) { assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.errCalled) - assert.Equal(t, u.e, len(v.data)) + assert.Len(t, v.data, u.e) m.ClearFilter() assert.Equal(t, 2, v.dataCalled) assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 0, v.errCalled) - assert.Equal(t, size, len(v.data)) + assert.Len(t, v.data, size) }) } } @@ -116,7 +116,7 @@ func TestLogStartStop(t *testing.T) { assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 0, v.clearCalled) assert.Equal(t, 1, v.errCalled) - assert.Equal(t, 2, len(v.data)) + assert.Len(t, v.data, 2) } func TestLogClear(t *testing.T) { @@ -139,7 +139,7 @@ func TestLogClear(t *testing.T) { assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.errCalled) - assert.Equal(t, 0, len(v.data)) + assert.Empty(t, v.data) } func TestLogBasic(t *testing.T) { @@ -191,7 +191,6 @@ func TestLogAppend(t *testing.T) { assert.Equal(t, 2, v.dataCalled) assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.errCalled) - // assert.Equal(t, append(items, data...).Lines(false), v.data) } func TestLogTimedout(t *testing.T) { @@ -230,7 +229,7 @@ func TestToggleAllContainers(t *testing.T) { defer cancel() m.ToggleAllContainers(ctx) - assert.Equal(t, "", m.GetContainer()) + assert.Empty(t, m.GetContainer()) m.ToggleAllContainers(ctx) assert.Equal(t, "blee", m.GetContainer()) } @@ -259,9 +258,9 @@ func newTestView() *testView { return &testView{} } -func (t *testView) LogCanceled() {} -func (t *testView) LogStop() {} -func (t *testView) LogResume() {} +func (*testView) LogCanceled() {} +func (*testView) LogStop() {} +func (*testView) LogResume() {} func (t *testView) LogChanged(ll [][]byte) { t.data = ll @@ -284,30 +283,26 @@ type testFactory struct{} var _ dao.Factory = testFactory{} -func (f testFactory) Client() client.Connection { +func (testFactory) Client() client.Connection { return nil } - -func (f testFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) { +func (testFactory) Get(*client.GVR, string, bool, labels.Selector) (runtime.Object, error) { return nil, nil } - -func (f testFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) { +func (testFactory) List(*client.GVR, string, bool, labels.Selector) ([]runtime.Object, error) { return nil, nil } - -func (f testFactory) ForResource(ns, gvr string) (informers.GenericInformer, error) { +func (testFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) { return nil, nil } - -func (f testFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { +func (testFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) { return nil, nil } -func (f testFactory) WaitForCacheSync() {} -func (f testFactory) Forwarders() watch.Forwarders { +func (testFactory) WaitForCacheSync() {} +func (testFactory) Forwarders() watch.Forwarders { return nil } -func (f testFactory) DeleteForwarder(string) {} +func (testFactory) DeleteForwarder(string) {} func makeFactory() dao.Factory { return testFactory{} diff --git a/internal/model/menu_hint.go b/internal/model/menu_hint.go index 2ac0ffc9..bd6c3aff 100644 --- a/internal/model/menu_hint.go +++ b/internal/model/menu_hint.go @@ -5,7 +5,6 @@ package model import ( "strconv" - "strings" ) // MenuHint represents keyboard mnemonic. @@ -51,5 +50,5 @@ func (h MenuHints) Less(i, j int) bool { if err1 != nil && err2 == nil { return false } - return strings.Compare(h[i].Description, h[j].Description) < 0 + return h[i].Description < h[j].Description } diff --git a/internal/model/pulse_health.go b/internal/model/pulse_health.go index 8e2027e8..2929d329 100644 --- a/internal/model/pulse_health.go +++ b/internal/model/pulse_health.go @@ -32,15 +32,15 @@ func NewPulseHealth(f dao.Factory) *PulseHealth { // List returns a canned collection of resources health. func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, error) { - gvrs := []string{ - "v1/pods", - "v1/events", - "apps/v1/replicasets", - "apps/v1/deployments", - "apps/v1/statefulsets", - "apps/v1/daemonsets", - "batch/v1/jobs", - "v1/persistentvolumes", + gvrs := []*client.GVR{ + client.PodGVR, + client.EvGVR, + client.RsGVR, + client.DpGVR, + client.StsGVR, + client.DsGVR, + client.CjGVR, + client.PcGVR, } hh := make([]runtime.Object, 0, 10) @@ -89,11 +89,11 @@ func (h *PulseHealth) checkMetrics(ctx context.Context) (health.Checks, error) { tcpu += m.TotalCPU tmem += m.TotalMEM } - c1 := health.NewCheck("cpu") + c1 := health.NewCheck(client.CpuGVR) c1.Set(health.S1, ccpu) c1.Set(health.S2, acpu) c1.Set(health.S3, tcpu) - c2 := health.NewCheck("mem") + c2 := health.NewCheck(client.MemGVR) c2.Set(health.S1, cmem) c2.Set(health.S2, amem) c2.Set(health.S3, tmem) @@ -101,19 +101,19 @@ func (h *PulseHealth) checkMetrics(ctx context.Context) (health.Checks, error) { return health.Checks{c1, c2}, nil } -func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check, error) { - meta, ok := Registry[gvr] +func (h *PulseHealth) check(ctx context.Context, ns string, gvr *client.GVR) (*health.Check, error) { + meta, ok := Registry[gvr.String()] if !ok { meta = ResourceMeta{ - DAO: &dao.Table{}, - Renderer: &render.Table{}, + DAO: new(dao.Table), + Renderer: new(render.Table), } } if meta.DAO == nil { meta.DAO = &dao.Resource{} } - meta.DAO.Init(h.factory, client.NewGVR(gvr)) + meta.DAO.Init(h.factory, gvr) oo, err := meta.DAO.List(ctx, ns) if err != nil { return nil, err diff --git a/internal/model/registry.go b/internal/model/registry.go index 6495ad17..d9bb7682 100644 --- a/internal/model/registry.go +++ b/internal/model/registry.go @@ -4,6 +4,7 @@ package model import ( + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render/helm" @@ -13,180 +14,180 @@ import ( // Registry tracks resources metadata. // BOZO!! Break up deps and merge into single registrar. var Registry = map[string]ResourceMeta{ - "workloads": { - DAO: &dao.Workload{}, - Renderer: &render.Workload{}, - }, // Custom... - "references": { - DAO: &dao.Reference{}, - Renderer: &render.Reference{}, + client.WkGVR.String(): { + DAO: new(dao.Workload), + Renderer: new(render.Workload), }, - "dir": { - DAO: &dao.Dir{}, - Renderer: &render.Dir{}, + client.RefGVR.String(): { + DAO: new(dao.Reference), + Renderer: new(render.Reference), }, - "pulses": { - DAO: &dao.Pulse{}, + client.DirGVR.String(): { + DAO: new(dao.Dir), + Renderer: new(render.Dir), }, - "helm": { - DAO: &dao.HelmChart{}, - Renderer: &helm.Chart{}, + client.PuGVR.String(): { + DAO: new(dao.Pulse), }, - "helm-history": { - DAO: &dao.HelmHistory{}, - Renderer: &helm.History{}, + client.HmGVR.String(): { + DAO: new(dao.HelmChart), + Renderer: new(helm.Chart), }, - "containers": { - DAO: &dao.Container{}, - Renderer: &render.Container{}, - TreeRenderer: &xray.Container{}, + client.HmhGVR.String(): { + DAO: new(dao.HelmHistory), + Renderer: new(helm.History), }, - "scans": { - DAO: &dao.ImageScan{}, - Renderer: &render.ImageScan{}, + client.CoGVR.String(): { + DAO: new(dao.Container), + Renderer: new(render.Container), + TreeRenderer: new(xray.Container), }, - "contexts": { - DAO: &dao.Context{}, - Renderer: &render.Context{}, + client.ScGVR.String(): { + DAO: new(dao.ImageScan), + Renderer: new(render.ImageScan), }, - "screendumps": { - DAO: &dao.ScreenDump{}, - Renderer: &render.ScreenDump{}, + client.CtGVR.String(): { + DAO: new(dao.Context), + Renderer: new(render.Context), }, - "rbac": { - DAO: &dao.Rbac{}, - Renderer: &render.Rbac{}, + client.SdGVR.String(): { + DAO: new(dao.ScreenDump), + Renderer: new(render.ScreenDump), }, - "policy": { - DAO: &dao.Policy{}, - Renderer: &render.Policy{}, + client.RbacGVR.String(): { + DAO: new(dao.Rbac), + Renderer: new(render.Rbac), }, - "users": { - DAO: &dao.Subject{}, - Renderer: &render.Subject{}, + client.PolGVR.String(): { + DAO: new(dao.Policy), + Renderer: new(render.Policy), }, - "groups": { - DAO: &dao.Subject{}, - Renderer: &render.Subject{}, + client.UsrGVR.String(): { + DAO: new(dao.Subject), + Renderer: new(render.Subject), }, - "portforwards": { - DAO: &dao.PortForward{}, - Renderer: &render.PortForward{}, + client.GrpGVR.String(): { + DAO: new(dao.Subject), + Renderer: new(render.Subject), }, - "benchmarks": { - DAO: &dao.Benchmark{}, - Renderer: &render.Benchmark{}, + client.PfGVR.String(): { + DAO: new(dao.PortForward), + Renderer: new(render.PortForward), }, - "aliases": { - DAO: &dao.Alias{}, - Renderer: &render.Alias{}, + client.BeGVR.String(): { + DAO: new(dao.Benchmark), + Renderer: new(render.Benchmark), + }, + client.AliGVR.String(): { + DAO: new(dao.Alias), + Renderer: new(render.Alias), }, // Core... - "v1/endpoints": { - Renderer: &render.Endpoints{}, + client.EpGVR.String(): { + Renderer: new(render.Endpoints), }, - "v1/pods": { - DAO: &dao.Pod{}, + client.PodGVR.String(): { + DAO: new(dao.Pod), Renderer: render.NewPod(), - TreeRenderer: &xray.Pod{}, + TreeRenderer: new(xray.Pod), }, - "v1/namespaces": { - DAO: &dao.Namespace{}, - Renderer: &render.Namespace{}, + client.NsGVR.String(): { + DAO: new(dao.Namespace), + Renderer: new(render.Namespace), }, - "v1/secrets": { - DAO: &dao.Secret{}, - Renderer: &render.Secret{}, + client.SecGVR.String(): { + DAO: new(dao.Secret), + Renderer: new(render.Secret), }, - "v1/configmaps": { - DAO: &dao.ConfigMap{}, - Renderer: &render.ConfigMap{}, + client.CmGVR.String(): { + DAO: new(dao.ConfigMap), + Renderer: new(render.ConfigMap), }, - "v1/nodes": { - DAO: &dao.Node{}, - Renderer: &render.Node{}, + client.NodeGVR.String(): { + DAO: new(dao.Node), + Renderer: new(render.Node), }, - "v1/services": { - DAO: &dao.Service{}, - Renderer: &render.Service{}, - TreeRenderer: &xray.Service{}, + client.SvcGVR.String(): { + DAO: new(dao.Service), + Renderer: new(render.Service), + TreeRenderer: new(xray.Service), }, - "v1/serviceaccounts": { - Renderer: &render.ServiceAccount{}, + client.SaGVR.String(): { + Renderer: new(render.ServiceAccount), }, - "v1/persistentvolumes": { - Renderer: &render.PersistentVolume{}, + client.PvGVR.String(): { + Renderer: new(render.PersistentVolume), }, - "v1/persistentvolumeclaims": { - Renderer: &render.PersistentVolumeClaim{}, + client.PvcGVR.String(): { + Renderer: new(render.PersistentVolumeClaim), }, // Apps... - "apps/v1/deployments": { - DAO: &dao.Deployment{}, - Renderer: &render.Deployment{}, - TreeRenderer: &xray.Deployment{}, + client.DpGVR.String(): { + DAO: new(dao.Deployment), + Renderer: new(render.Deployment), + TreeRenderer: new(xray.Deployment), }, - "apps/v1/replicasets": { - Renderer: &render.ReplicaSet{}, - TreeRenderer: &xray.ReplicaSet{}, + client.RsGVR.String(): { + Renderer: new(render.ReplicaSet), + TreeRenderer: new(xray.ReplicaSet), }, - "apps/v1/statefulsets": { - DAO: &dao.StatefulSet{}, - Renderer: &render.StatefulSet{}, - TreeRenderer: &xray.StatefulSet{}, + client.StsGVR.String(): { + DAO: new(dao.StatefulSet), + Renderer: new(render.StatefulSet), + TreeRenderer: new(xray.StatefulSet), }, - "apps/v1/daemonsets": { - DAO: &dao.DaemonSet{}, - Renderer: &render.DaemonSet{}, - TreeRenderer: &xray.DaemonSet{}, + client.DsGVR.String(): { + DAO: new(dao.DaemonSet), + Renderer: new(render.DaemonSet), + TreeRenderer: new(xray.DaemonSet), }, // Extensions... - "networking.k8s.io/v1/networkpolicies": { + client.NpGVR.String(): { Renderer: &render.NetworkPolicy{}, }, // Batch... - "batch/v1/cronjobs": { - DAO: &dao.CronJob{}, - Renderer: &render.CronJob{}, + client.CjGVR.String(): { + DAO: new(dao.CronJob), + Renderer: new(render.CronJob), }, - "batch/v1/jobs": { - DAO: &dao.Job{}, - Renderer: &render.Job{}, + client.JobGVR.String(): { + DAO: new(dao.Job), + Renderer: new(render.Job), }, // CRDs... - "apiextensions.k8s.io/v1/customresourcedefinitions": { - DAO: &dao.CustomResourceDefinition{}, - Renderer: &render.CustomResourceDefinition{}, + client.CrdGVR.String(): { + DAO: new(dao.CustomResourceDefinition), + Renderer: new(render.CustomResourceDefinition), }, // Storage... - "storage.k8s.io/v1/storageclasses": { + client.ScGVR.String(): { Renderer: &render.StorageClass{}, }, // Policy... - "policy/v1/poddisruptionbudgets": { + client.PdbGVR.String(): { Renderer: &render.PodDisruptionBudget{}, }, // RBAC... - "rbac.authorization.k8s.io/v1/clusterroles": { - DAO: &dao.Rbac{}, - Renderer: &render.ClusterRole{}, + client.CrGVR.String(): { + DAO: new(dao.Rbac), + Renderer: new(render.ClusterRole), }, - "rbac.authorization.k8s.io/v1/clusterrolebindings": { - Renderer: &render.ClusterRoleBinding{}, + client.CrbGVR.String(): { + Renderer: new(render.ClusterRoleBinding), }, - "rbac.authorization.k8s.io/v1/roles": { - Renderer: &render.Role{}, + client.RoGVR.String(): { + Renderer: new(render.Role), }, - "rbac.authorization.k8s.io/v1/rolebindings": { - Renderer: &render.RoleBinding{}, + client.RobGVR.String(): { + Renderer: new(render.RoleBinding), }, } diff --git a/internal/model/rev_values.go b/internal/model/rev_values.go index 059fa0ef..e41bd599 100644 --- a/internal/model/rev_values.go +++ b/internal/model/rev_values.go @@ -5,6 +5,7 @@ package model import ( "context" + "errors" "log/slog" "strings" "sync/atomic" @@ -20,7 +21,7 @@ import ( // RevValues tracks Helm values representations. type RevValues struct { - gvr client.GVR + gvr *client.GVR inUpdate int32 path string rev string @@ -32,7 +33,7 @@ type RevValues struct { } // NewRevValues return a new Helm values resource model. -func NewRevValues(gvr client.GVR, path, rev string) *RevValues { +func NewRevValues(gvr *client.GVR, path, rev string) *RevValues { return &RevValues{ gvr: gvr, path: path, @@ -43,10 +44,10 @@ func NewRevValues(gvr client.GVR, path, rev string) *RevValues { } func getHelmHistDao() *dao.HelmHistory { - return Registry["helm-history"].DAO.(*dao.HelmHistory) + return Registry[client.HmhGVR.String()].DAO.(*dao.HelmHistory) } -func getRevValues(path, rev string) []string { +func getRevValues(path, _ string) []string { vals, err := getHelmHistDao().GetValues(path, true) if err != nil { slog.Error("Failed to get Helm values", slogs.Error, err) @@ -55,7 +56,7 @@ func getRevValues(path, rev string) []string { } // GVR returns the resource gvr. -func (v *RevValues) GVR() client.GVR { +func (v *RevValues) GVR() *client.GVR { return v.gvr } @@ -157,24 +158,20 @@ func (v *RevValues) updater(ctx context.Context) { } } -func (v *RevValues) refresh(ctx context.Context) error { +func (v *RevValues) refresh(context.Context) error { if !atomic.CompareAndSwapInt32(&v.inUpdate, 0, 1) { slog.Debug("Dropping update...") - return nil + return errors.New("refresh in progress, dropping") } defer atomic.StoreInt32(&v.inUpdate, 0) - if err := v.reconcile(ctx); err != nil { - return err - } + v.reconcile() return nil } -func (v *RevValues) reconcile(_ context.Context) error { +func (v *RevValues) reconcile() { v.fireResourceChanged(v.lines, v.filter(v.query, v.lines)) - - return nil } // AddListener adds a new model listener. diff --git a/internal/model/stack.go b/internal/model/stack.go index d3d8446c..b48862ce 100644 --- a/internal/model/stack.go +++ b/internal/model/stack.go @@ -97,9 +97,7 @@ func (s *Stack) Push(c Component) { } s.mx.Lock() - { - s.components = append(s.components, c) - } + s.components = append(s.components, c) s.mx.Unlock() s.notify(StackPush, c) } @@ -112,12 +110,11 @@ func (s *Stack) Pop() (Component, bool) { var c Component s.mx.Lock() - { - c = s.components[len(s.components)-1] - c.Stop() - s.components = s.components[:len(s.components)-1] - } + c = s.components[len(s.components)-1] + c.Stop() + s.components = s.components[:len(s.components)-1] s.mx.Unlock() + s.notify(StackPop, c) return c, true diff --git a/internal/model/stack_test.go b/internal/model/stack_test.go index 4f4f73e3..15a21d85 100644 --- a/internal/model/stack_test.go +++ b/internal/model/stack_test.go @@ -76,7 +76,7 @@ func TestStackPrevious(t *testing.T) { for _, c := range u.items { s.Push(c) } - for i := 0; i < u.pops; i++ { + for range u.pops { s.Pop() } assert.Equal(t, u.e, s.Previous()) @@ -111,7 +111,7 @@ func TestStackIsLast(t *testing.T) { for _, c := range u.items { s.Push(c) } - for i := 0; i < u.pops; i++ { + for range u.pops { s.Pop() } assert.Equal(t, u.e, s.IsLast()) @@ -142,7 +142,7 @@ func TestStackFlatten(t *testing.T) { s.Push(c) } assert.Equal(t, u.e, s.Flatten()) - assert.Equal(t, len(u.e), len(s.Peek())) + assert.Len(t, s.Peek(), len(u.e)) }) } } @@ -180,7 +180,7 @@ func TestStackPush(t *testing.T) { for _, c := range u.items { s.Push(c) } - for i := 0; i < u.pop; i++ { + for range u.pop { s.Pop() } assert.Equal(t, u.e, s.Empty()) @@ -279,9 +279,10 @@ func (s *stackL) StackPushed(model.Component) { s.count++ } -func (s *stackL) StackPopped(c, top model.Component) { +func (s *stackL) StackPopped(_, _ model.Component) { s.count-- } + func (s *stackL) StackTop(model.Component) { s.tops++ } type c struct { @@ -303,13 +304,13 @@ func (c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return ni func (c) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { return nil } -func (c c) SetRect(int, int, int, int) {} -func (c c) GetRect() (int, int, int, int) { return 0, 0, 0, 0 } -func (c c) GetFocusable() tview.Focusable { return nil } -func (c c) Focus(func(tview.Primitive)) {} -func (c c) Blur() {} -func (c c) Start() {} -func (c c) Stop() {} -func (c c) Init(context.Context) error { return nil } -func (c c) SetFilter(string) {} -func (c c) SetLabelFilter(map[string]string) {} +func (c) SetRect(int, int, int, int) {} +func (c) GetRect() (a, b, c, d int) { return 0, 0, 0, 0 } +func (c) GetFocusable() tview.Focusable { return nil } +func (c) Focus(func(tview.Primitive)) {} +func (c) Blur() {} +func (c) Start() {} +func (c) Stop() {} +func (c) Init(context.Context) error { return nil } +func (c) SetFilter(string) {} +func (c) SetLabelFilter(map[string]string) {} diff --git a/internal/model/table.go b/internal/model/table.go index 2c0c8dc3..06a9827a 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -35,7 +35,7 @@ type TableListener interface { // Table represents a table model. type Table struct { - gvr client.GVR + gvr *client.GVR data *model1.TableData listeners []TableListener inUpdate int32 @@ -47,7 +47,7 @@ type Table struct { } // NewTable returns a new table model. -func NewTable(gvr client.GVR) *Table { +func NewTable(gvr *client.GVR) *Table { return &Table{ gvr: gvr, data: model1.NewTableData(gvr), @@ -57,9 +57,7 @@ func NewTable(gvr client.GVR) *Table { func (t *Table) SetViewSetting(ctx context.Context, vs *config.ViewSetting) { t.mx.Lock() - { - t.vs = vs - } + t.vs = vs t.mx.Unlock() if ctx != context.Background() { @@ -264,7 +262,9 @@ func (t *Table) reconcile(ctx context.Context) error { err error ) meta := resourceMeta(t.gvr) - meta.DAO.SetIncludeObject(true) + if t.vs != nil { + meta.DAO.SetIncludeObject(true) + } ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) if t.instance == "" { oo, err = t.list(ctx, meta.DAO) @@ -278,7 +278,7 @@ func (t *Table) reconcile(ctx context.Context) error { r := meta.Renderer r.SetViewSetting(t.vs) - return t.data.Reconcile(ctx, meta.Renderer, oo) + return t.data.Render(ctx, meta.Renderer, oo) } func (t *Table) fireTableChanged(data *model1.TableData) { diff --git a/internal/model/table_int_test.go b/internal/model/table_int_test.go index ee522a49..19677f5f 100644 --- a/internal/model/table_int_test.go +++ b/internal/model/table_int_test.go @@ -17,6 +17,7 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/watch" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -24,7 +25,7 @@ import ( ) func TestTableReconcile(t *testing.T) { - ta := NewTable(client.NewGVR("v1/pods")) + ta := NewTable(client.PodGVR) ta.SetNamespace(client.NamespaceAll) f := makeFactory() @@ -33,7 +34,7 @@ func TestTableReconcile(t *testing.T) { ctx = context.WithValue(ctx, internal.KeyFields, "") ctx = context.WithValue(ctx, internal.KeyWithMetrics, false) err := ta.reconcile(ctx) - assert.Nil(t, err) + require.NoError(t, err) data := ta.Peek() assert.Equal(t, 25, data.HeaderCount()) assert.Equal(t, 1, data.RowCount()) @@ -41,18 +42,18 @@ func TestTableReconcile(t *testing.T) { } func TestTableList(t *testing.T) { - ta := NewTable(client.NewGVR("v1/pods")) + ta := NewTable(client.PodGVR) ta.SetNamespace("blee") acc := accessor{} ctx := context.WithValue(context.Background(), internal.KeyFactory, makeFactory()) rows, err := ta.list(ctx, &acc) - assert.Nil(t, err) - assert.Equal(t, 1, len(rows)) + require.NoError(t, err) + assert.Len(t, rows, 1) } func TestTableGet(t *testing.T) { - ta := NewTable(client.NewGVR("v1/pods")) + ta := NewTable(client.PodGVR) ta.SetNamespace("blee") f := makeFactory() @@ -60,24 +61,24 @@ func TestTableGet(t *testing.T) { ctx := context.WithValue(context.Background(), internal.KeyFactory, f) ctx = context.WithValue(ctx, internal.KeyWithMetrics, false) row, err := ta.Get(ctx, "fred") - assert.Nil(t, err) + require.NoError(t, err) assert.NotNil(t, row) - assert.Equal(t, 5, len(row.(*render.PodWithMetrics).Raw.Object)) + assert.Len(t, row.(*render.PodWithMetrics).Raw.Object, 5) } func TestTableMeta(t *testing.T) { uu := map[string]struct { - gvr string + gvr *client.GVR accessor dao.Accessor renderer model1.Renderer }{ "generic": { - gvr: "containers", + gvr: client.CoGVR, accessor: &dao.Container{}, renderer: &render.Container{}, }, "node": { - gvr: "v1/nodes", + gvr: client.NodeGVR, accessor: &dao.Node{}, renderer: &render.Node{}, }, @@ -85,7 +86,7 @@ func TestTableMeta(t *testing.T) { for k := range uu { u := uu[k] - ta := NewTable(client.NewGVR(u.gvr)) + ta := NewTable(u.gvr) m := resourceMeta(ta.gvr) assert.Equal(t, u.accessor, m.DAO) @@ -110,10 +111,10 @@ func mustLoad(n string) *unstructured.Unstructured { func load(t *testing.T, 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 } @@ -129,56 +130,56 @@ type testFactory struct { var _ dao.Factory = testFactory{} -func (f testFactory) Client() client.Connection { +func (testFactory) Client() client.Connection { return client.NewTestAPIClient() } -func (f testFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) { +func (f testFactory) Get(*client.GVR, string, bool, labels.Selector) (runtime.Object, error) { if len(f.rows) > 0 { return f.rows[0], nil } return nil, nil } -func (f testFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) { +func (f testFactory) List(*client.GVR, string, bool, labels.Selector) ([]runtime.Object, error) { if len(f.rows) > 0 { return f.rows, nil } return nil, nil } -func (f testFactory) ForResource(ns, gvr string) (informers.GenericInformer, error) { +func (testFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) { return nil, nil } -func (f testFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { +func (testFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) { return nil, nil } -func (f testFactory) WaitForCacheSync() {} -func (f testFactory) Forwarders() watch.Forwarders { +func (testFactory) WaitForCacheSync() {} +func (testFactory) Forwarders() watch.Forwarders { return nil } -func (f testFactory) DeleteForwarder(string) {} +func (testFactory) DeleteForwarder(string) {} // ---------------------------------------------------------------------------- type accessor struct { - gvr client.GVR + gvr *client.GVR } var _ dao.Accessor = (*accessor)(nil) -func (a *accessor) SetIncludeObject(bool) {} +func (*accessor) SetIncludeObject(bool) {} -func (a *accessor) List(ctx context.Context, ns string) ([]runtime.Object, error) { +func (*accessor) List(context.Context, string) ([]runtime.Object, error) { return []runtime.Object{&render.PodWithMetrics{Raw: mustLoad("p1")}}, nil } -func (a *accessor) Get(ctx context.Context, path string) (runtime.Object, error) { +func (*accessor) Get(context.Context, string) (runtime.Object, error) { return &render.PodWithMetrics{Raw: mustLoad("p1")}, nil } -func (a *accessor) Init(_ dao.Factory, gvr client.GVR) { +func (a *accessor) Init(_ dao.Factory, gvr *client.GVR) { a.gvr = gvr } diff --git a/internal/model/table_test.go b/internal/model/table_test.go index e2df5c09..a877c5b8 100644 --- a/internal/model/table_test.go +++ b/internal/model/table_test.go @@ -17,6 +17,7 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/watch" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -24,7 +25,7 @@ import ( ) func TestTableRefresh(t *testing.T) { - ta := model.NewTable(client.NewGVR("v1/pods")) + ta := model.NewTable(client.PodGVR) ta.SetNamespace(client.NamespaceAll) l := tableListener{} @@ -34,7 +35,7 @@ func TestTableRefresh(t *testing.T) { ctx := context.WithValue(context.Background(), internal.KeyFactory, f) ctx = context.WithValue(ctx, internal.KeyFields, "") ctx = context.WithValue(ctx, internal.KeyWithMetrics, false) - assert.NoError(t, ta.Refresh(ctx)) + require.NoError(t, ta.Refresh(ctx)) data := ta.Peek() assert.Equal(t, 25, data.HeaderCount()) assert.Equal(t, 1, data.RowCount()) @@ -44,7 +45,7 @@ func TestTableRefresh(t *testing.T) { } func TestTableNS(t *testing.T) { - ta := model.NewTable(client.NewGVR("v1/pods")) + ta := model.NewTable(client.PodGVR) ta.SetNamespace("blee") assert.Equal(t, "blee", ta.GetNamespace()) @@ -53,7 +54,7 @@ func TestTableNS(t *testing.T) { } func TestTableAddListener(t *testing.T) { - ta := model.NewTable(client.NewGVR("v1/pods")) + ta := model.NewTable(client.PodGVR) ta.SetNamespace("blee") assert.True(t, ta.Empty()) @@ -61,8 +62,8 @@ func TestTableAddListener(t *testing.T) { ta.AddListener(&l) } -func TestTableRmListener(t *testing.T) { - ta := model.NewTable(client.NewGVR("v1/pods")) +func TestTableRmListener(*testing.T) { + ta := model.NewTable(client.PodGVR) ta.SetNamespace("blee") l := tableListener{} @@ -89,36 +90,36 @@ type tableFactory struct { var _ dao.Factory = tableFactory{} -func (f tableFactory) Client() client.Connection { +func (tableFactory) Client() client.Connection { return client.NewTestAPIClient() } -func (f tableFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) { +func (f tableFactory) Get(*client.GVR, string, bool, labels.Selector) (runtime.Object, error) { if len(f.rows) > 0 { return f.rows[0], nil } return nil, nil } -func (f tableFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) { +func (f tableFactory) List(*client.GVR, string, bool, labels.Selector) ([]runtime.Object, error) { if len(f.rows) > 0 { return f.rows, nil } return nil, nil } -func (f tableFactory) ForResource(ns, gvr string) (informers.GenericInformer, error) { +func (tableFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) { return nil, nil } -func (f tableFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { +func (tableFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) { return nil, nil } -func (f tableFactory) WaitForCacheSync() {} -func (f tableFactory) Forwarders() watch.Forwarders { +func (tableFactory) WaitForCacheSync() {} +func (tableFactory) Forwarders() watch.Forwarders { return nil } -func (f tableFactory) DeleteForwarder(string) {} +func (tableFactory) DeleteForwarder(string) {} func makeTableFactory() tableFactory { return tableFactory{} diff --git a/internal/model/text_test.go b/internal/model/text_test.go index ffa390cd..65084ea4 100644 --- a/internal/model/text_test.go +++ b/internal/model/text_test.go @@ -84,7 +84,7 @@ func (l *textLis) TextChanged(ll []string) { l.changed++ } -func (l *textLis) TextFiltered(ll []string, mm fuzzy.Matches) { +func (l *textLis) TextFiltered(_ []string, mm fuzzy.Matches) { l.matches = len(mm) l.filtered++ if len(mm) > 0 && len(mm[0].MatchedIndexes) > 0 { diff --git a/internal/model/tree.go b/internal/model/tree.go index f97b5852..c31b60ca 100644 --- a/internal/model/tree.go +++ b/internal/model/tree.go @@ -35,7 +35,7 @@ type TreeListener interface { // Tree represents a tree model. type Tree struct { - gvr client.GVR + gvr *client.GVR namespace string root *xray.TreeNode listeners []TreeListener @@ -45,7 +45,7 @@ type Tree struct { } // NewTree returns a new model. -func NewTree(gvr client.GVR) *Tree { +func NewTree(gvr *client.GVR) *Tree { return &Tree{ gvr: gvr, refreshRate: 2 * time.Second, @@ -133,7 +133,7 @@ func (t *Tree) Peek() *xray.TreeNode { } // Describe describes a given resource. -func (t *Tree) Describe(ctx context.Context, gvr, path string) (string, error) { +func (t *Tree) Describe(ctx context.Context, gvr *client.GVR, path string) (string, error) { meta, err := t.getMeta(ctx, gvr) if err != nil { return "", err @@ -148,7 +148,7 @@ func (t *Tree) Describe(ctx context.Context, gvr, path string) (string, error) { } // ToYAML returns a resource yaml. -func (t *Tree) ToYAML(ctx context.Context, gvr, path string) (string, error) { +func (t *Tree) ToYAML(ctx context.Context, gvr *client.GVR, path string) (string, error) { meta, err := t.getMeta(ctx, gvr) if err != nil { return "", err @@ -210,8 +210,7 @@ func (t *Tree) reconcile(ctx context.Context) error { } ns := client.CleanseNamespace(t.namespace) - res := t.gvr.R() - root := xray.NewTreeNode(res, res) + root := xray.NewTreeNode(t.gvr, t.gvr.R()) ctx = context.WithValue(ctx, xray.KeyParent, root) if _, ok := meta.TreeRenderer.(*xray.Generic); ok { table, ok := oo[0].(*metav1.Table) @@ -264,13 +263,13 @@ func (t *Tree) fireTreeLoadFailed(err error) { } } -func (t *Tree) getMeta(ctx context.Context, gvr string) (ResourceMeta, error) { +func (t *Tree) getMeta(ctx context.Context, gvr *client.GVR) (ResourceMeta, error) { meta := t.resourceMeta() factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory) if !ok { return ResourceMeta{}, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory)) } - meta.DAO.Init(factory, client.NewGVR(gvr)) + meta.DAO.Init(factory, gvr) return meta, nil } @@ -294,10 +293,15 @@ func treeHydrate(ctx context.Context, ns string, oo []runtime.Object, re TreeRen if re == nil { return fmt.Errorf("no tree renderer defined for this resource") } + pool := internal.NewWorkerPool(ctx, internal.DefaultPoolSize) for _, o := range oo { - if err := re.Render(ctx, ns, o); err != nil { - return err - } + pool.Add(func(_ context.Context) error { + return re.Render(ctx, ns, o) + }) + } + errs := pool.Drain() + if len(errs) > 0 { + return errs[0] } return nil diff --git a/internal/model/types.go b/internal/model/types.go index d175a0c2..9d7d9225 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -34,7 +34,7 @@ type ViewerToggleOpts map[string]bool type ResourceViewer interface { GetPath() string Filter(string) - GVR() client.GVR + GVR() *client.GVR ClearFilter() Peek() []string SetOptions(context.Context, ViewerToggleOpts) @@ -136,7 +136,7 @@ type Describer interface { // TreeRenderer represents an xray node. type TreeRenderer interface { - Render(ctx context.Context, ns string, o interface{}) error + Render(ctx context.Context, ns string, o any) error } // ResourceMeta represents model info about a resource. diff --git a/internal/model/values.go b/internal/model/values.go index 54efcc79..40b2ea35 100644 --- a/internal/model/values.go +++ b/internal/model/values.go @@ -22,7 +22,7 @@ import ( // Values tracks Helm values representations. type Values struct { factory dao.Factory - gvr client.GVR + gvr *client.GVR inUpdate int32 path string query string @@ -33,7 +33,7 @@ type Values struct { } // NewValues return a new Helm values resource model. -func NewValues(gvr client.GVR, path string) *Values { +func NewValues(gvr *client.GVR, path string) *Values { return &Values{ gvr: gvr, path: path, @@ -71,7 +71,7 @@ func (v *Values) getValues() ([]string, error) { } // GVR returns the resource gvr. -func (v *Values) GVR() client.GVR { +func (v *Values) GVR() *client.GVR { return v.gvr } @@ -186,24 +186,20 @@ func (v *Values) updater(ctx context.Context) { } } -func (v *Values) refresh(ctx context.Context) error { +func (v *Values) refresh(context.Context) error { if !atomic.CompareAndSwapInt32(&v.inUpdate, 0, 1) { slog.Debug("Dropping update...") - return nil + return fmt.Errorf("reconcile in progress. Dropping update") } defer atomic.StoreInt32(&v.inUpdate, 0) - if err := v.reconcile(ctx); err != nil { - return err - } + v.reconcile() return nil } -func (v *Values) reconcile(_ context.Context) error { +func (v *Values) reconcile() { v.fireResourceChanged(v.lines, v.filter(v.query, v.lines)) - - return nil } // AddListener adds a new model listener. diff --git a/internal/model/yaml.go b/internal/model/yaml.go index b6ecc2b6..407cc302 100644 --- a/internal/model/yaml.go +++ b/internal/model/yaml.go @@ -25,7 +25,7 @@ const ManagedFieldsOpts = "ManagedFields" // YAML tracks yaml resource representations. type YAML struct { - gvr client.GVR + gvr *client.GVR inUpdate int32 path string query string @@ -35,7 +35,7 @@ type YAML struct { } // NewYAML return a new yaml resource model. -func NewYAML(gvr client.GVR, path string) *YAML { +func NewYAML(gvr *client.GVR, path string) *YAML { return &YAML{ gvr: gvr, path: path, @@ -43,7 +43,7 @@ func NewYAML(gvr client.GVR, path string) *YAML { } // GVR returns the resource gvr. -func (y *YAML) GVR() client.GVR { +func (y *YAML) GVR() *client.GVR { return y.gvr } @@ -195,7 +195,7 @@ func (y *YAML) RemoveListener(l ResourceViewerListener) { } // ToYAML returns a resource yaml. -func (y *YAML) ToYAML(ctx context.Context, gvr client.GVR, path string, showManaged bool) (string, error) { +func (*YAML) ToYAML(ctx context.Context, gvr *client.GVR, path string, showManaged bool) (string, error) { meta, err := getMeta(ctx, gvr) if err != nil { return "", err diff --git a/internal/model1/color.go b/internal/model1/color.go index f570a040..0b69f712 100644 --- a/internal/model1/color.go +++ b/internal/model1/color.go @@ -37,7 +37,7 @@ func DefaultColorer(ns string, h Header, re *RowEvent) tcell.Color { return ErrColor } - // nolint:exhaustive + //nolint:exhaustive switch re.Kind { case EventAdd: return AddColor diff --git a/internal/model1/header_test.go b/internal/model1/header_test.go index 922656b4..f4bc4208 100644 --- a/internal/model1/header_test.go +++ b/internal/model1/header_test.go @@ -297,7 +297,7 @@ func TestHeaderClone(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { c := u.h.Clone() - assert.Equal(t, len(u.h), len(c)) + assert.Len(t, u.h, len(c)) if len(u.h) > 0 { u.h[0].Name = "blee" assert.Equal(t, "A", c[0].Name) diff --git a/internal/model1/helpers.go b/internal/model1/helpers.go index 5ee4541e..e6458a7b 100644 --- a/internal/model1/helpers.go +++ b/internal/model1/helpers.go @@ -4,7 +4,9 @@ package model1 import ( + "context" "fmt" + "log/slog" "math" "sort" "strings" @@ -15,11 +17,24 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +const poolSize = 10 + func Hydrate(ns string, oo []runtime.Object, rr Rows, re Renderer) error { + pool := NewWorkerPool(context.Background(), poolSize) for i, o := range oo { - if err := re.Render(o, ns, &rr[i]); err != nil { - return err - } + pool.Add(func(ctx context.Context) error { + select { + case <-ctx.Done(): + slog.Debug("Worker canceled") + return nil + default: + return re.Render(o, ns, &rr[i]) + } + }) + } + errs := pool.Drain() + if len(errs) > 0 { + return errs[0] } return nil @@ -31,17 +46,28 @@ func GenericHydrate(ns string, table *metav1.Table, rr Rows, re Renderer) error return fmt.Errorf("expecting generic renderer but got %T", re) } gr.SetTable(ns, table) + pool := NewWorkerPool(context.Background(), poolSize) for i, row := range table.Rows { - if err := gr.Render(row, ns, &rr[i]); err != nil { - return err - } + pool.Add(func(ctx context.Context) error { + select { + case <-ctx.Done(): + slog.Debug("Worker canceled") + return nil + default: + return gr.Render(row, ns, &rr[i]) + } + }) + } + errs := pool.Drain() + if len(errs) > 0 { + return errs[0] } return nil } // IsValid returns true if resource is valid, false otherwise. -func IsValid(ns string, h Header, r Row) bool { +func IsValid(_ string, h Header, r Row) bool { if len(r.Fields) == 0 { return true } @@ -80,7 +106,7 @@ func labelize(labels string) map[string]string { } func durationToSeconds(duration string) int64 { - if len(duration) == 0 { + if duration == "" { return 0 } if duration == NAValue { diff --git a/internal/model1/helpers_test.go b/internal/model1/helpers_test.go index e76e9f84..fcc6f24c 100644 --- a/internal/model1/helpers_test.go +++ b/internal/model1/helpers_test.go @@ -98,7 +98,6 @@ func TestIsValid(t *testing.T) { assert.Equal(t, u.e, valid) }) } - } func TestDurationToSecond(t *testing.T) { @@ -131,7 +130,7 @@ func BenchmarkDurationToSecond(b *testing.B) { b.ReportAllocs() b.ResetTimer() - for n := 0; n < b.N; n++ { + for range b.N { durationToSeconds(t) } } diff --git a/internal/model1/pool.go b/internal/model1/pool.go new file mode 100644 index 00000000..7a07976b --- /dev/null +++ b/internal/model1/pool.go @@ -0,0 +1,77 @@ +package model1 + +import ( + "context" + "log/slog" + "sync" + + "github.com/derailed/k9s/internal/slogs" +) + +type jobFn func(ctx context.Context) error + +type WorkerPool struct { + semC chan struct{} + errC chan error + ctx context.Context + cancelFn context.CancelFunc + mx sync.RWMutex + wg sync.WaitGroup + wge sync.WaitGroup + errs []error +} + +func NewWorkerPool(ctx context.Context, size int) *WorkerPool { + _, cancelFn := context.WithCancel(ctx) + + p := WorkerPool{ + semC: make(chan struct{}, size), + errC: make(chan error, 1), + cancelFn: cancelFn, + ctx: ctx, + } + + p.wge.Add(1) + go func(wg *sync.WaitGroup) { + defer wg.Done() + for err := range p.errC { + if err != nil { + p.mx.Lock() + p.errs = append(p.errs, err) + p.mx.Unlock() + } + } + }(&p.wge) + + return &p +} + +func (p *WorkerPool) Add(job jobFn) { + p.semC <- struct{}{} + p.wg.Add(1) + go func(ctx context.Context, wg *sync.WaitGroup, semC <-chan struct{}, errC chan<- error) { + defer func() { + <-semC + wg.Done() + }() + if err := job(ctx); err != nil { + slog.Error("Worker error", slogs.Error, err) + errC <- err + } + }(p.ctx, &p.wg, p.semC, p.errC) +} + +func (p *WorkerPool) Drain() []error { + if p.cancelFn != nil { + p.cancelFn() + p.cancelFn = nil + } + p.wg.Wait() + close(p.semC) + close(p.errC) + p.wge.Wait() + + p.mx.RLock() + defer p.mx.RUnlock() + return p.errs +} diff --git a/internal/model1/pool_test.go b/internal/model1/pool_test.go new file mode 100644 index 00000000..e02b471e --- /dev/null +++ b/internal/model1/pool_test.go @@ -0,0 +1,57 @@ +package model1_test + +import ( + "context" + "fmt" + "sync/atomic" + "testing" + + "github.com/derailed/k9s/internal/model1" + "github.com/stretchr/testify/assert" +) + +func TestWorkerPoolPlain(t *testing.T) { + p := model1.NewWorkerPool(context.Background(), 2) + + var c atomic.Int32 + for range 10 { + p.Add(func(ctx context.Context) error { + select { + case <-ctx.Done(): + fmt.Println("Worker canceled") + return nil + default: + c.Add(1) + return nil + } + }) + } + errs := p.Drain() + assert.Equal(t, 10, int(c.Load())) + assert.Empty(t, errs) +} + +func TestWorkerPoolWithError(t *testing.T) { + ctx := context.Background() + p := model1.NewWorkerPool(ctx, 2) + + var c atomic.Int32 + for i := range 10 { + p.Add(func(ctx context.Context) error { + select { + case <-ctx.Done(): + fmt.Println("Worker canceled") + return nil + default: + if i%2 == 0 { + return fmt.Errorf("BOOM%d", i) + } + c.Add(1) + return nil + } + }) + } + errs := p.Drain() + assert.Equal(t, 5, int(c.Load())) + assert.Len(t, errs, 5) +} diff --git a/internal/model1/row_event_test.go b/internal/model1/row_event_test.go index 9ca93229..02c4a06f 100644 --- a/internal/model1/row_event_test.go +++ b/internal/model1/row_event_test.go @@ -9,6 +9,7 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRowEventCustomize(t *testing.T) { @@ -406,7 +407,7 @@ func TestRowEventsDelete(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.NoError(t, u.re.Delete(u.id)) + require.NoError(t, u.re.Delete(u.id)) assert.Equal(t, u.e, u.re) }) } diff --git a/internal/model1/row_test.go b/internal/model1/row_test.go index 55c5cccf..fed63dea 100644 --- a/internal/model1/row_test.go +++ b/internal/model1/row_test.go @@ -18,7 +18,7 @@ func BenchmarkRowCustomize(b *testing.B) { cols := []int{0, 1, 2} b.ReportAllocs() b.ResetTimer() - for n := 0; n < b.N; n++ { + for range b.N { _ = row.Customize(cols) } } diff --git a/internal/model1/table_data.go b/internal/model1/table_data.go index dc25afb2..f622b9ac 100644 --- a/internal/model1/table_data.go +++ b/internal/model1/table_data.go @@ -19,6 +19,7 @@ import ( "github.com/sahilm/fuzzy" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" ) // SortFn represent a function that can sort columnar data. @@ -48,26 +49,26 @@ type TableData struct { header Header rowEvents *RowEvents namespace string - gvr client.GVR + gvr *client.GVR mx sync.RWMutex } // NewTableData returns a new table. -func NewTableData(gvr client.GVR) *TableData { +func NewTableData(gvr *client.GVR) *TableData { return &TableData{ gvr: gvr, rowEvents: NewRowEvents(10), } } -func NewTableDataFull(gvr client.GVR, ns string, h Header, re *RowEvents) *TableData { +func NewTableDataFull(gvr *client.GVR, ns string, h Header, re *RowEvents) *TableData { t := NewTableDataWithRows(gvr, h, re) t.namespace = ns return t } -func NewTableDataWithRows(gvr client.GVR, h Header, re *RowEvents) *TableData { +func NewTableDataWithRows(gvr *client.GVR, h Header, re *RowEvents) *TableData { t := NewTableData(gvr) t.header, t.rowEvents = h, re @@ -130,7 +131,7 @@ func (t *TableData) HeaderCount() int { return len(t.header) } -func (t *TableData) HeadCol(n string, w bool) (HeaderColumn, int) { +func (t *TableData) HeadCol(n string, w bool) (header HeaderColumn, idx int) { idx, ok := t.header.IndexOf(n, w) if !ok { return HeaderColumn{}, -1 @@ -242,15 +243,13 @@ func (t *TableData) GetNamespace() string { func (t *TableData) Reset(ns string) { t.mx.Lock() - { - t.namespace = ns - } + t.namespace = ns t.mx.Unlock() t.Clear() } -func (t *TableData) Reconcile(ctx context.Context, r Renderer, oo []runtime.Object) error { +func (t *TableData) Render(_ context.Context, r Renderer, oo []runtime.Object) error { var rows Rows if len(oo) > 0 { if r.IsGeneric() { @@ -425,32 +424,30 @@ func (t *TableData) SetHeader(ns string, h Header) { // Update computes row deltas and update the table data. func (t *TableData) Update(rows Rows) { empty := t.Empty() - kk := make(map[string]struct{}, len(rows)) + kk := sets.New[string]() var blankDelta DeltaRow t.mx.Lock() - { - for _, row := range rows { - kk[row.ID] = struct{}{} - if empty { - t.rowEvents.Add(NewRowEvent(EventAdd, row)) - continue - } - if index, ok := t.rowEvents.FindIndex(row.ID); ok { - ev, ok := t.rowEvents.At(index) - if !ok { - continue - } - delta := NewDeltaRow(ev.Row, row, t.header) - if delta.IsBlank() { - ev.Kind, ev.Deltas, ev.Row = EventUnchanged, blankDelta, row - t.rowEvents.Set(index, ev) - } else { - t.rowEvents.Set(index, NewRowEventWithDeltas(row, delta)) - } - continue - } + for _, row := range rows { + kk.Insert(row.ID) + if empty { t.rowEvents.Add(NewRowEvent(EventAdd, row)) + continue } + if index, ok := t.rowEvents.FindIndex(row.ID); ok { + ev, ok := t.rowEvents.At(index) + if !ok { + continue + } + delta := NewDeltaRow(ev.Row, row, t.header) + if delta.IsBlank() { + ev.Kind, ev.Deltas, ev.Row = EventUnchanged, blankDelta, row + t.rowEvents.Set(index, ev) + } else { + t.rowEvents.Set(index, NewRowEventWithDeltas(row, delta)) + } + continue + } + t.rowEvents.Add(NewRowEvent(EventAdd, row)) } t.mx.Unlock() @@ -460,28 +457,28 @@ func (t *TableData) Update(rows Rows) { } // Delete removes items in cache that are no longer valid. -func (t *TableData) Delete(newKeys map[string]struct{}) { +func (t *TableData) Delete(newKeys sets.Set[string]) { t.mx.Lock() - { - victims := make([]string, 0, 10) - t.rowEvents.Range(func(_ int, e RowEvent) bool { - if _, ok := newKeys[e.Row.ID]; !ok { - victims = append(victims, e.Row.ID) - } else { - delete(newKeys, e.Row.ID) - } - return true - }) - for _, id := range victims { - if err := t.rowEvents.Delete(id); err != nil { - slog.Error("Table delete failed", - slogs.Error, err, - slogs.Message, id, - ) - } + defer t.mx.Unlock() + + victims := sets.New[string]() + t.rowEvents.Range(func(_ int, e RowEvent) bool { + if newKeys.Has(e.Row.ID) { + delete(newKeys, e.Row.ID) + } else { + victims.Insert(e.Row.ID) + } + return true + }) + + for _, id := range victims.UnsortedList() { + if err := t.rowEvents.Delete(id); err != nil { + slog.Error("Table delete failed", + slogs.Error, err, + slogs.Message, id, + ) } } - t.mx.Unlock() } // Diff checks if two tables are equal. diff --git a/internal/model1/table_data_test.go b/internal/model1/table_data_test.go index 185f2cac..01dc8939 100644 --- a/internal/model1/table_data_test.go +++ b/internal/model1/table_data_test.go @@ -10,6 +10,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/sets" ) func init() { @@ -329,7 +330,7 @@ func TestTableDataUpdate(t *testing.T) { func TestTableDataDelete(t *testing.T) { uu := map[string]struct { re, e *RowEvents - kk map[string]struct{} + kk sets.Set[string] }{ "ordered": { re: NewRowEventsWithEvts( @@ -337,7 +338,7 @@ func TestTableDataDelete(t *testing.T) { RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, ), - kk: map[string]struct{}{"A": {}, "C": {}}, + kk: sets.New[string]("A", "C"), e: NewRowEventsWithEvts( RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, @@ -350,7 +351,7 @@ func TestTableDataDelete(t *testing.T) { RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, RowEvent{Row: Row{ID: "D", Fields: Fields{"10", "2", "3"}}}, ), - kk: map[string]struct{}{"C": {}, "A": {}}, + kk: sets.New[string]("C", "A"), e: NewRowEventsWithEvts( RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, diff --git a/internal/model1/types.go b/internal/model1/types.go index 8235ca3c..b7b5004e 100644 --- a/internal/model1/types.go +++ b/internal/model1/types.go @@ -40,7 +40,7 @@ type Renderer interface { IsGeneric() bool // Render converts raw resources to tabular data. - Render(o interface{}, ns string, row *Row) error + Render(o any, ns string, row *Row) error // Header returns the resource header. Header(ns string) Header @@ -61,5 +61,5 @@ type Generic interface { Header(ns string) Header // Render renders the resource. - Render(o interface{}, ns string, row *Row) error + Render(o any, ns string, row *Row) error } diff --git a/internal/perf/benchmark.go b/internal/perf/benchmark.go index e6ff7f47..41a24681 100644 --- a/internal/perf/benchmark.go +++ b/internal/perf/benchmark.go @@ -33,14 +33,14 @@ const ( // Benchmark puts a workload under load. type Benchmark struct { canceled bool - config config.BenchConfig + config *config.BenchConfig worker *requester.Work cancelFn context.CancelFunc mx sync.RWMutex } // NewBenchmark returns a new benchmark. -func NewBenchmark(base, version string, cfg config.BenchConfig) (*Benchmark, error) { +func NewBenchmark(base, version string, cfg *config.BenchConfig) (*Benchmark, error) { b := Benchmark{config: cfg} if err := b.init(base, version); err != nil { return nil, err @@ -51,7 +51,7 @@ func NewBenchmark(base, version string, cfg config.BenchConfig) (*Benchmark, err func (b *Benchmark) init(base, version string) error { var ctx context.Context ctx, b.cancelFn = context.WithTimeout(context.Background(), benchTimeout) - req, err := http.NewRequestWithContext(ctx, b.config.HTTP.Method, base, nil) + req, err := http.NewRequestWithContext(ctx, b.config.HTTP.Method, base, http.NoBody) if err != nil { return err } @@ -133,8 +133,8 @@ func (b *Benchmark) save(cluster, context string, r io.Reader) error { return err } bf := filepath.Join(dir, fmt.Sprintf(benchFmat, ns, n, time.Now().UnixNano())) - if err := data.EnsureDirPath(bf, data.DefaultDirMod); err != nil { - return err + if e := data.EnsureDirPath(bf, data.DefaultDirMod); e != nil { + return e } f, err := os.Create(bf) diff --git a/internal/pool.go b/internal/pool.go new file mode 100644 index 00000000..095ba679 --- /dev/null +++ b/internal/pool.go @@ -0,0 +1,79 @@ +package internal + +import ( + "context" + "log/slog" + "sync" + + "github.com/derailed/k9s/internal/slogs" +) + +const DefaultPoolSize = 10 + +type jobFn func(ctx context.Context) error + +type WorkerPool struct { + semC chan struct{} + errC chan error + ctx context.Context + cancelFn context.CancelFunc + mx sync.RWMutex + wg sync.WaitGroup + wge sync.WaitGroup + errs []error +} + +func NewWorkerPool(ctx context.Context, size int) *WorkerPool { + _, cancelFn := context.WithCancel(ctx) + + p := WorkerPool{ + semC: make(chan struct{}, size), + errC: make(chan error, 1), + cancelFn: cancelFn, + ctx: ctx, + } + + p.wge.Add(1) + go func(wg *sync.WaitGroup) { + defer wg.Done() + for err := range p.errC { + if err != nil { + p.mx.Lock() + p.errs = append(p.errs, err) + p.mx.Unlock() + } + } + }(&p.wge) + + return &p +} + +func (p *WorkerPool) Add(job jobFn) { + p.semC <- struct{}{} + p.wg.Add(1) + go func(ctx context.Context, wg *sync.WaitGroup, semC <-chan struct{}, errC chan<- error) { + defer func() { + <-semC + wg.Done() + }() + if err := job(ctx); err != nil { + slog.Error("Worker error", slogs.Error, err) + errC <- err + } + }(p.ctx, &p.wg, p.semC, p.errC) +} + +func (p *WorkerPool) Drain() []error { + if p.cancelFn != nil { + p.cancelFn() + p.cancelFn = nil + } + p.wg.Wait() + close(p.semC) + close(p.errC) + p.wge.Wait() + + p.mx.RLock() + defer p.mx.RUnlock() + return p.errs +} diff --git a/internal/pool_test.go b/internal/pool_test.go new file mode 100644 index 00000000..f8973f55 --- /dev/null +++ b/internal/pool_test.go @@ -0,0 +1,57 @@ +package internal_test + +import ( + "context" + "fmt" + "sync/atomic" + "testing" + + "github.com/derailed/k9s/internal" + "github.com/stretchr/testify/assert" +) + +func TestWorkerPoolPlain(t *testing.T) { + p := internal.NewWorkerPool(context.Background(), 2) + + var c atomic.Int32 + for range 10 { + p.Add(func(ctx context.Context) error { + select { + case <-ctx.Done(): + fmt.Println("Worker canceled") + return nil + default: + c.Add(1) + return nil + } + }) + } + errs := p.Drain() + assert.Equal(t, 10, int(c.Load())) + assert.Empty(t, errs) +} + +func TestWorkerPoolWithError(t *testing.T) { + ctx := context.Background() + p := internal.NewWorkerPool(ctx, 2) + + var c atomic.Int32 + for i := range 10 { + p.Add(func(ctx context.Context) error { + select { + case <-ctx.Done(): + fmt.Println("Worker canceled") + return nil + default: + if i%2 == 0 { + return fmt.Errorf("BOOM%d", i) + } + c.Add(1) + return nil + } + }) + } + errs := p.Drain() + assert.Equal(t, 5, int(c.Load())) + assert.Len(t, errs, 5) +} diff --git a/internal/port/co_portspec_test.go b/internal/port/co_portspec_test.go index 39f57040..9417aba9 100644 --- a/internal/port/co_portspec_test.go +++ b/internal/port/co_portspec_test.go @@ -8,6 +8,7 @@ import ( "github.com/derailed/k9s/internal/port" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestContainerPortSpecMatch(t *testing.T) { @@ -62,7 +63,7 @@ func TestContainerPortSpecMatch(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { pf, err := port.ParsePF(u.ann) - assert.Nil(t, err) + require.NoError(t, err) assert.Equal(t, u.e, u.spec.Match(pf)) }) @@ -134,7 +135,7 @@ func TestContainerPortSpecsMatch(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { pf, err := port.ParsePF(u.ann) - assert.Nil(t, err) + require.NoError(t, err) assert.Equal(t, u.e, u.specs.Match(pf)) }) } diff --git a/internal/port/pf.go b/internal/port/pf.go index 9b56fbe4..8339dda7 100644 --- a/internal/port/pf.go +++ b/internal/port/pf.go @@ -35,7 +35,7 @@ type PFAnn struct { } func ParsePlainPF(ann string) (*PFAnn, error) { - if len(ann) == 0 { + if ann == "" { return nil, fmt.Errorf("invalid annotation %q", ann) } var pf PFAnn @@ -43,7 +43,7 @@ func ParsePlainPF(ann string) (*PFAnn, error) { if len(mm) < 3 { return nil, fmt.Errorf("invalid plain port-forward %s", ann) } - if len(mm[2]) == 0 { + if mm[2] == "" { pf.ContainerPort = intstr.Parse(mm[1]) pf.LocalPort = mm[1] return &pf, nil diff --git a/internal/port/pf_test.go b/internal/port/pf_test.go index 66a11d15..5f415140 100644 --- a/internal/port/pf_test.go +++ b/internal/port/pf_test.go @@ -9,6 +9,7 @@ import ( "github.com/derailed/k9s/internal/port" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -143,7 +144,7 @@ func TestPFPortNum(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { pf, err := port.ParsePF(u.exp) - assert.Nil(t, err) + require.NoError(t, err) n, err := pf.PortNum() assert.Equal(t, u.err, err) if err != nil { @@ -184,7 +185,7 @@ func TestPFToTunnel(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { pf, err := port.ParsePF(u.exp) - assert.Nil(t, err) + require.NoError(t, err) pt, err := pf.ToTunnel("blee") assert.Equal(t, u.err, err) if err != nil { @@ -215,7 +216,7 @@ func TestPFString(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { pf, err := port.ParsePF(u.exp) - assert.Nil(t, err) + require.NoError(t, err) assert.Equal(t, u.e, pf.String()) }) } diff --git a/internal/port/pfs.go b/internal/port/pfs.go index 85b0a2e5..9dd9e760 100644 --- a/internal/port/pfs.go +++ b/internal/port/pfs.go @@ -15,7 +15,7 @@ type PortChecker func(PortTunnel) bool type PFAnns []*PFAnn // ToPortSpec returns a container port and local port definitions. -func (aa PFAnns) ToPortSpec(pp ContainerPortSpecs) (string, string) { +func (aa PFAnns) ToPortSpec(pp ContainerPortSpecs) (ports, localPorts string) { specs, lps := make([]string, 0, len(aa)), make([]string, 0, len(aa)) for _, a := range aa { specs = append(specs, a.AsSpec()) @@ -32,7 +32,7 @@ func (aa PFAnns) ToPortSpec(pp ContainerPortSpecs) (string, string) { return strings.Join(specs, ","), strings.Join(lps, ",") } -func (aa PFAnns) ToTunnels(address string, pp ContainerPortSpecs, available PortChecker) (PortTunnels, error) { +func (aa PFAnns) ToTunnels(address string, _ ContainerPortSpecs, available PortChecker) (PortTunnels, error) { pts := make(PortTunnels, 0, len(aa)) for _, a := range aa { pt, err := a.ToTunnel(address) diff --git a/internal/port/pfs_test.go b/internal/port/pfs_test.go index 3d003a47..e7d7bf5f 100644 --- a/internal/port/pfs_test.go +++ b/internal/port/pfs_test.go @@ -9,6 +9,7 @@ import ( "github.com/derailed/k9s/internal/port" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -100,7 +101,7 @@ func TestPFsToTunnel(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { pfs, err := port.ParsePFs(u.exp) - assert.Nil(t, err) + require.NoError(t, err) pts, err := pfs.ToTunnels("fred", u.specs, f) assert.Equal(t, u.e, err) if err != nil { diff --git a/internal/render/alias.go b/internal/render/alias.go index 6b7f841e..3fc4d736 100644 --- a/internal/render/alias.go +++ b/internal/render/alias.go @@ -14,36 +14,37 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) +var defaultAliasHeader = model1.Header{ + model1.HeaderColumn{Name: "RESOURCE"}, + model1.HeaderColumn{Name: "GROUP"}, + model1.HeaderColumn{Name: "VERSION"}, + model1.HeaderColumn{Name: "COMMAND"}, +} + // Alias renders an aliases to screen. type Alias struct { Base } // Header returns a header row. -func (Alias) Header(ns string) model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "RESOURCE"}, - model1.HeaderColumn{Name: "GROUP"}, - model1.HeaderColumn{Name: "VERSION"}, - model1.HeaderColumn{Name: "COMMAND"}, - } +func (Alias) Header(string) model1.Header { + return defaultAliasHeader } // Render renders a K8s resource to screen. // BOZO!! Pass in a row with pre-alloc fields?? -func (Alias) Render(o interface{}, ns string, r *model1.Row) error { +func (Alias) Render(o any, _ string, r *model1.Row) error { a, ok := o.(AliasRes) if !ok { return fmt.Errorf("expected AliasRes, but got %T", o) } - slices.Sort(a.Aliases) - gvr := client.NewGVR(a.GVR) - r.ID = gvr.String() + + r.ID = a.GVR.String() r.Fields = append(r.Fields, - gvr.R(), - gvr.G(), - gvr.V(), + a.GVR.R(), + a.GVR.G(), + a.GVR.V(), strings.Join(a.Aliases, " "), ) @@ -55,7 +56,7 @@ func (Alias) Render(o interface{}, ns string, r *model1.Row) error { // AliasRes represents an alias resource. type AliasRes struct { - GVR string + GVR *client.GVR Aliases []string } diff --git a/internal/render/alias_test.go b/internal/render/alias_test.go index b29bca0f..a6a453aa 100644 --- a/internal/render/alias_test.go +++ b/internal/render/alias_test.go @@ -11,6 +11,7 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAliasColorer(t *testing.T) { @@ -68,12 +69,12 @@ func TestAliasRender(t *testing.T) { var a render.Alias o := render.AliasRes{ - GVR: "fred/v1/blee", + GVR: client.NewGVR("fred/v1/blee"), Aliases: []string{"a", "b", "c"}, } var r model1.Row - assert.Nil(t, a.Render(o, "fred/v1/blee", &r)) + require.NoError(t, a.Render(o, "fred/v1/blee", &r)) assert.Equal(t, model1.Row{ ID: "fred/v1/blee", Fields: model1.Fields{"blee", "fred", "v1", "a b c"}, @@ -82,15 +83,15 @@ func TestAliasRender(t *testing.T) { func BenchmarkAlias(b *testing.B) { o := render.AliasRes{ - GVR: "fred/v1/blee", + GVR: client.NewGVR("fred/v1/blee"), Aliases: []string{"a", "b", "c"}, } var a render.Alias b.ResetTimer() b.ReportAllocs() - for i := 0; i < b.N; i++ { + for range b.N { var r model1.Row - _ = a.Render(o, "aliases", &r) + _ = a.Render(o, "ns-1", &r) } } diff --git a/internal/render/base.go b/internal/render/base.go index c86c17db..f3f35eb3 100644 --- a/internal/render/base.go +++ b/internal/render/base.go @@ -15,9 +15,7 @@ import ( type DecoratorFunc func(string) string // AgeDecorator represents a timestamped as human column. -var AgeDecorator = func(a string) string { - return toAgeHuman(a) -} +var AgeDecorator = toAgeHuman type Base struct { vs *config.ViewSetting diff --git a/internal/render/benchmark.go b/internal/render/benchmark.go index c2cc433e..dca82656 100644 --- a/internal/render/benchmark.go +++ b/internal/render/benchmark.go @@ -24,7 +24,7 @@ var ( totalRx = regexp.MustCompile(`Total:\s+([0-9.]+)\ssecs`) reqRx = regexp.MustCompile(`Requests/sec:\s+([0-9.]+)`) okRx = regexp.MustCompile(`\[2\d{2}\]\s+(\d+)\s+responses`) - errRx = regexp.MustCompile(`\[[4-5]\d{2}\]\s+(\d+)\s+responses`) + errRx = regexp.MustCompile(`\[[45]\d{2}\]\s+(\d+)\s+responses`) toastRx = regexp.MustCompile(`Error distribution`) ) @@ -34,7 +34,7 @@ type Benchmark struct { } // ColorerFunc colors a resource row. -func (b Benchmark) ColorerFunc() model1.ColorerFunc { +func (Benchmark) ColorerFunc() model1.ColorerFunc { return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { if !model1.IsValid(ns, h, re.Row) { return model1.ErrColor @@ -45,7 +45,7 @@ func (b Benchmark) ColorerFunc() model1.ColorerFunc { } // Header returns a header row. -func (Benchmark) Header(ns string) model1.Header { +func (Benchmark) Header(string) model1.Header { return model1.Header{ model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAME"}, @@ -61,7 +61,7 @@ func (Benchmark) Header(ns string) model1.Header { } // Render renders a K8s resource to screen. -func (b Benchmark) Render(o interface{}, ns string, r *model1.Row) error { +func (b Benchmark) Render(o any, ns string, r *model1.Row) error { bench, ok := o.(BenchInfo) if !ok { return fmt.Errorf("no benchmarks available %T", o) @@ -111,7 +111,7 @@ func (Benchmark) readFile(file string) (string, error) { return string(data), nil } -func (b Benchmark) initRow(row model1.Fields, f os.FileInfo) error { +func (Benchmark) initRow(row model1.Fields, f os.FileInfo) error { tokens := strings.Split(f.Name(), "_") if len(tokens) < 2 { return fmt.Errorf("invalid file name %s", f.Name()) @@ -125,7 +125,7 @@ func (b Benchmark) initRow(row model1.Fields, f os.FileInfo) error { } func (b Benchmark) augmentRow(fields model1.Fields, data string) { - if len(data) == 0 { + if data == "" { return } @@ -164,7 +164,7 @@ func (Benchmark) countReq(rr [][]string) string { var sum int for _, m := range rr { - if m, err := strconv.Atoi(string(m[1])); err == nil { + if m, err := strconv.Atoi(m[1]); err == nil { sum += m } } diff --git a/internal/render/benchmark_int_test.go b/internal/render/benchmark_int_test.go index bb6c42ed..3de18e55 100644 --- a/internal/render/benchmark_int_test.go +++ b/internal/render/benchmark_int_test.go @@ -10,6 +10,7 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func init() { @@ -44,7 +45,7 @@ func TestAugmentRow(t *testing.T) { t.Run(k, func(t *testing.T) { data, err := os.ReadFile(u.file) - assert.Nil(t, err) + require.NoError(t, err) fields := make(model1.Fields, 8) b := Benchmark{} b.augmentRow(fields, string(data)) diff --git a/internal/render/cm.go b/internal/render/cm.go index 2dde05b8..73009cff 100644 --- a/internal/render/cm.go +++ b/internal/render/cm.go @@ -21,21 +21,19 @@ type ConfigMap struct { // Header returns a header row. func (m ConfigMap) Header(_ string) model1.Header { - return m.doHeader(m.defaultHeader()) + return m.doHeader(defaultCMHeader) } -func (ConfigMap) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "DATA"}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } +var defaultCMHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "DATA"}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, } // Render renders a K8s resource to screen. -func (m ConfigMap) Render(o interface{}, ns string, row *model1.Row) error { +func (m ConfigMap) Render(o any, _ string, row *model1.Row) error { if err := m.defaultRow(o, row); err != nil { return err } @@ -43,7 +41,7 @@ func (m ConfigMap) Render(o interface{}, ns string, row *model1.Row) error { return nil } - cols, err := m.specs.realize(o.(*unstructured.Unstructured), m.defaultHeader(), row) + cols, err := m.specs.realize(o.(*unstructured.Unstructured), defaultCMHeader, row) if err != nil { return err } @@ -53,10 +51,10 @@ func (m ConfigMap) Render(o interface{}, ns string, row *model1.Row) error { } // Render renders a K8s resource to screen. -func (ConfigMap) defaultRow(o interface{}, r *model1.Row) error { +func (ConfigMap) defaultRow(o any, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected ConfigMap, but got %T", o) + return fmt.Errorf("expected *Unstructured, but got %T", o) } var cm v1.ConfigMap err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cm) diff --git a/internal/render/container.go b/internal/render/container.go index c05242a0..0039e9a6 100644 --- a/internal/render/container.go +++ b/internal/render/container.go @@ -20,6 +20,8 @@ import ( mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) +const falseStr = "false" + // ContainerWithMetrics represents a container and it's metrics. type ContainerWithMetrics interface { // Container returns the container @@ -44,7 +46,7 @@ type Container struct { } // ColorerFunc colors a resource row. -func (c Container) ColorerFunc() model1.ColorerFunc { +func (Container) ColorerFunc() model1.ColorerFunc { return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { c := model1.DefaultColorer(ns, h, re) @@ -70,37 +72,35 @@ func (c Container) ColorerFunc() model1.ColorerFunc { } // Header returns a header row. -func (c Container) Header(_ string) model1.Header { - return c.defaultHeader() +func (Container) Header(_ string) model1.Header { + return defaultCOHeader } // Header returns a header row. -func (Container) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "IDX"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "PF"}, - model1.HeaderColumn{Name: "IMAGE"}, - model1.HeaderColumn{Name: "READY"}, - model1.HeaderColumn{Name: "STATE"}, - model1.HeaderColumn{Name: "RESTARTS", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "PROBES(L:R:S)"}, - model1.HeaderColumn{Name: "CPU", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "MEM", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "CPU/RL", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "MEM/RL", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "%CPU/R", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "%CPU/L", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "%MEM/R", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "%MEM/L", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "PORTS"}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } +var defaultCOHeader = model1.Header{ + model1.HeaderColumn{Name: "IDX"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "PF"}, + model1.HeaderColumn{Name: "IMAGE"}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "STATE"}, + model1.HeaderColumn{Name: "RESTARTS", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "PROBES(L:R:S)"}, + model1.HeaderColumn{Name: "CPU", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "MEM", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "CPU/RL", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "MEM/RL", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "%CPU/R", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "%CPU/L", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "%MEM/R", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "%MEM/L", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "PORTS"}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, } // Render renders a K8s resource to screen. -func (c Container) Render(o interface{}, ns string, row *model1.Row) error { +func (c Container) Render(o any, _ string, row *model1.Row) error { cr, ok := o.(ContainerRes) if !ok { return fmt.Errorf("expected ContainerRes, but got %T", o) @@ -111,7 +111,7 @@ func (c Container) Render(o interface{}, ns string, row *model1.Row) error { func (c Container) defaultRow(cr ContainerRes, r *model1.Row) error { cur, res := gatherMetrics(cr.Container, cr.MX) - ready, state, restarts := "false", MissingValue, "0" + ready, state, restarts := falseStr, MissingValue, "0" if cr.Status != nil { ready, state, restarts = boolToStr(cr.Status.Ready), ToContainerState(cr.Status.State), strconv.Itoa(int(cr.Status.RestartCount)) } @@ -148,7 +148,7 @@ func (Container) diagnose(state, ready string) error { return nil } - if ready == "false" { + if ready == falseStr { return errors.New("container is not ready") } return nil @@ -200,7 +200,7 @@ func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, r metric func ToContainerPorts(pp []v1.ContainerPort) string { ports := make([]string, len(pp)) for i, p := range pp { - if len(p.Name) > 0 { + if p.Name != "" { ports[i] = p.Name + ":" } ports[i] += strconv.Itoa(int(p.ContainerPort)) @@ -255,7 +255,7 @@ type ContainerRes struct { } // GetObjectKind returns a schema object. -func (c ContainerRes) GetObjectKind() schema.ObjectKind { +func (ContainerRes) GetObjectKind() schema.ObjectKind { return nil } diff --git a/internal/render/container_test.go b/internal/render/container_test.go index d7f4f6f3..46169743 100644 --- a/internal/render/container_test.go +++ b/internal/render/container_test.go @@ -11,6 +11,7 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -27,7 +28,7 @@ func TestContainer(t *testing.T) { Age: makeAge(), } var r model1.Row - assert.Nil(t, c.Render(cres, "blee", &r)) + require.NoError(t, c.Render(cres, "blee", &r)) assert.Equal(t, "fred", r.ID) assert.Equal(t, model1.Fields{ "", @@ -54,19 +55,20 @@ func TestContainer(t *testing.T) { } func BenchmarkContainerRender(b *testing.B) { - var c render.Container - - cres := render.ContainerRes{ - Container: makeContainer(), - Status: makeContainerStatus(), - MX: makeContainerMetrics(), - Age: makeAge(), - } - var r model1.Row + var ( + c render.Container + r model1.Row + cres = render.ContainerRes{ + Container: makeContainer(), + Status: makeContainerStatus(), + MX: makeContainerMetrics(), + Age: makeAge(), + } + ) b.ReportAllocs() b.ResetTimer() - for n := 0; n < b.N; n++ { + for range b.N { _ = c.Render(cres, "blee", &r) } } diff --git a/internal/render/context.go b/internal/render/context.go index 2aa17368..30cdaf69 100644 --- a/internal/render/context.go +++ b/internal/render/context.go @@ -34,7 +34,7 @@ func (Context) ColorerFunc() model1.ColorerFunc { } // Header returns a header row. -func (Context) Header(ns string) model1.Header { +func (Context) Header(string) model1.Header { return model1.Header{ model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "CLUSTER"}, @@ -44,7 +44,7 @@ func (Context) Header(ns string) model1.Header { } // Render renders a K8s resource to screen. -func (c Context) Render(o interface{}, _ string, r *model1.Row) error { +func (Context) Render(o any, _ string, r *model1.Row) error { ctx, ok := o.(*NamedContext) if !ok { return fmt.Errorf("expected *NamedContext, but got %T", o) @@ -96,7 +96,7 @@ func (c *NamedContext) IsCurrentContext(n string) bool { } // GetObjectKind returns a schema object. -func (c *NamedContext) GetObjectKind() schema.ObjectKind { +func (*NamedContext) GetObjectKind() schema.ObjectKind { return nil } diff --git a/internal/render/context_test.go b/internal/render/context_test.go index 1cdc3911..d921b99c 100644 --- a/internal/render/context_test.go +++ b/internal/render/context_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/client-go/tools/clientcmd/api" ) func TestContextHeader(t *testing.T) { var c render.Context - assert.Equal(t, 4, len(c.Header(""))) + assert.Len(t, c.Header(""), 4) } func TestContextRender(t *testing.T) { @@ -48,7 +49,7 @@ func TestContextRender(t *testing.T) { row := model1.NewRow(4) err := r.Render(uc.ctx, "", &row) - assert.Nil(t, err) + require.NoError(t, err) assert.Equal(t, uc.e, row) }) } @@ -59,6 +60,6 @@ func TestContextRender(t *testing.T) { type config struct{} -func (k config) CurrentContextName() (string, error) { +func (config) CurrentContextName() (string, error) { return "fred", nil } diff --git a/internal/render/cr.go b/internal/render/cr.go index 033e3388..eed69a6e 100644 --- a/internal/render/cr.go +++ b/internal/render/cr.go @@ -20,23 +20,21 @@ type ClusterRole struct { // Header returns a header row. func (c ClusterRole) Header(_ string) model1.Header { - return c.doHeader(c.defaultHeader()) + return c.doHeader(defaultCRHeader) } // Header returns a header rbw. -func (ClusterRole) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } +var defaultCRHeader = model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, } // Render renders a K8s resource to screen. -func (p ClusterRole) Render(o interface{}, ns string, row *model1.Row) error { +func (p ClusterRole) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expecting clusterrole, but got %T", o) + return fmt.Errorf("expecting Unstructured, but got %T", o) } if err := p.defaultRow(raw, row); err != nil { return err @@ -44,8 +42,7 @@ func (p ClusterRole) Render(o interface{}, ns string, row *model1.Row) error { if p.specs.isEmpty() { return nil } - - cols, err := p.specs.realize(raw, p.defaultHeader(), row) + cols, err := p.specs.realize(raw, defaultCRHeader, row) if err != nil { return err } diff --git a/internal/render/cr_test.go b/internal/render/cr_test.go index d6d17531..1f0ca608 100644 --- a/internal/render/cr_test.go +++ b/internal/render/cr_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestClusterRoleRender(t *testing.T) { c := render.ClusterRole{} r := model1.NewRow(2) - assert.NoError(t, c.Render(load(t, "cr"), "-", &r)) + require.NoError(t, c.Render(load(t, "cr"), "-", &r)) assert.Equal(t, "-/blee", r.ID) assert.Equal(t, model1.Fields{"blee"}, r.Fields[:1]) } diff --git a/internal/render/crb.go b/internal/render/crb.go index 7b8712fd..b41eb77c 100644 --- a/internal/render/crb.go +++ b/internal/render/crb.go @@ -13,6 +13,15 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +var defaultCRBHeader = model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "CLUSTERROLE"}, + model1.HeaderColumn{Name: "SUBJECT-KIND"}, + model1.HeaderColumn{Name: "SUBJECTS"}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // ClusterRoleBinding renders a K8s ClusterRoleBinding to screen. type ClusterRoleBinding struct { Base @@ -20,26 +29,14 @@ type ClusterRoleBinding struct { // Header returns a header row. func (c ClusterRoleBinding) Header(_ string) model1.Header { - return c.doHeader(c.defaultHeader()) -} - -// Header returns a header rbw. -func (ClusterRoleBinding) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "CLUSTERROLE"}, - model1.HeaderColumn{Name: "SUBJECT-KIND"}, - model1.HeaderColumn{Name: "SUBJECTS"}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return c.doHeader(defaultCRBHeader) } // Render renders a K8s resource to screen. -func (c ClusterRoleBinding) Render(o interface{}, ns string, row *model1.Row) error { +func (c ClusterRoleBinding) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected ClusterRoleBinding, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } if err := c.defaultRow(raw, row); err != nil { return err @@ -48,8 +45,7 @@ func (c ClusterRoleBinding) Render(o interface{}, ns string, row *model1.Row) er return nil } - // !BOZO!! Call header 2 times - cols, err := c.specs.realize(raw, c.defaultHeader(), row) + cols, err := c.specs.realize(raw, defaultCRBHeader, row) if err != nil { return err } diff --git a/internal/render/crb_test.go b/internal/render/crb_test.go index 4b350a4c..ce10fca7 100644 --- a/internal/render/crb_test.go +++ b/internal/render/crb_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestClusterRoleBindingRender(t *testing.T) { c := render.ClusterRoleBinding{} r := model1.NewRow(5) - assert.NoError(t, c.Render(load(t, "crb"), "-", &r)) + require.NoError(t, c.Render(load(t, "crb"), "-", &r)) assert.Equal(t, "-/blee", r.ID) assert.Equal(t, model1.Fields{"blee", "blee", "User", "fernand"}, r.Fields[:4]) } diff --git a/internal/render/crd.go b/internal/render/crd.go index a5bb5af7..f8d19707 100644 --- a/internal/render/crd.go +++ b/internal/render/crd.go @@ -17,6 +17,18 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +var defaultCRDHeader = model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "GROUP"}, + model1.HeaderColumn{Name: "KIND"}, + model1.HeaderColumn{Name: "VERSIONS"}, + model1.HeaderColumn{Name: "SCOPE"}, + model1.HeaderColumn{Name: "ALIASES", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // CustomResourceDefinition renders a K8s CustomResourceDefinition to screen. type CustomResourceDefinition struct { Base @@ -24,29 +36,14 @@ type CustomResourceDefinition struct { // Header returns a header row. func (c CustomResourceDefinition) Header(_ string) model1.Header { - return c.doHeader(c.defaultHeader()) -} - -// Header returns a header rbw. -func (CustomResourceDefinition) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "GROUP"}, - model1.HeaderColumn{Name: "KIND"}, - model1.HeaderColumn{Name: "VERSIONS"}, - model1.HeaderColumn{Name: "SCOPE"}, - model1.HeaderColumn{Name: "ALIASES", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return c.doHeader(defaultCRDHeader) } // Render renders a K8s resource to screen. -func (c CustomResourceDefinition) Render(o interface{}, ns string, row *model1.Row) error { +func (c CustomResourceDefinition) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected CustomResourceDefinition, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } if err := c.defaultRow(raw, row); err != nil { @@ -55,9 +52,7 @@ func (c CustomResourceDefinition) Render(o interface{}, ns string, row *model1.R if c.specs.isEmpty() { return nil } - - // !BOZO!! Call header 2 times - cols, err := c.specs.realize(raw, c.defaultHeader(), row) + cols, err := c.specs.realize(raw, defaultCRDHeader, row) if err != nil { return err } @@ -88,7 +83,7 @@ func (c CustomResourceDefinition) defaultRow(raw *unstructured.Unstructured, r * slog.Warn("Unable to assert CRD versions", slogs.FQN, crd.Name) } - r.ID = client.MetaFQN(crd.ObjectMeta) + r.ID = client.MetaFQN(&crd.ObjectMeta) r.Fields = model1.Fields{ crd.Spec.Names.Plural, crd.Spec.Group, @@ -104,7 +99,7 @@ func (c CustomResourceDefinition) defaultRow(raw *unstructured.Unstructured, r * return nil } -func (c CustomResourceDefinition) diagnose(n string, vv []v1.CustomResourceDefinitionVersion) error { +func (CustomResourceDefinition) diagnose(n string, vv []v1.CustomResourceDefinitionVersion) error { if len(vv) == 0 { return fmt.Errorf("unable to assert CRD servers versions for %s", n) } diff --git a/internal/render/crd_test.go b/internal/render/crd_test.go index a88715ee..33ba8eb5 100644 --- a/internal/render/crd_test.go +++ b/internal/render/crd_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCustomResourceDefinitionRender(t *testing.T) { c := render.CustomResourceDefinition{} r := model1.NewRow(2) - assert.NoError(t, c.Render(load(t, "crd"), "", &r)) + require.NoError(t, c.Render(load(t, "crd"), "", &r)) assert.Equal(t, "-/adapters.config.istio.io", r.ID) assert.Equal(t, "adapters", r.Fields[0]) assert.Equal(t, "config.istio.io", r.Fields[1]) diff --git a/internal/render/cronjob.go b/internal/render/cronjob.go index ca1cd1dc..72d12cfb 100644 --- a/internal/render/cronjob.go +++ b/internal/render/cronjob.go @@ -16,6 +16,22 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +var defaultCJHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, + model1.HeaderColumn{Name: "SCHEDULE"}, + model1.HeaderColumn{Name: "SUSPEND"}, + model1.HeaderColumn{Name: "ACTIVE"}, + model1.HeaderColumn{Name: "LAST_SCHEDULE", Attrs: model1.Attrs{Time: true}}, + model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "CONTAINERS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "IMAGES", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // CronJob renders a K8s CronJob to screen. type CronJob struct { Base @@ -23,32 +39,14 @@ type CronJob struct { // Header returns a header row. func (c CronJob) Header(_ string) model1.Header { - return c.doHeader(c.defaultHeader()) -} - -func (CronJob) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, - model1.HeaderColumn{Name: "SCHEDULE"}, - model1.HeaderColumn{Name: "SUSPEND"}, - model1.HeaderColumn{Name: "ACTIVE"}, - model1.HeaderColumn{Name: "LAST_SCHEDULE", Attrs: model1.Attrs{Time: true}}, - model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "CONTAINERS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "IMAGES", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return c.doHeader(defaultCJHeader) } // Render renders a K8s resource to screen. -func (c CronJob) Render(o interface{}, ns string, row *model1.Row) error { +func (c CronJob) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected CronJob, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } if err := c.defaultRow(raw, row); err != nil { return err @@ -57,7 +55,7 @@ func (c CronJob) Render(o interface{}, ns string, row *model1.Row) error { return nil } - cols, err := c.specs.realize(raw, c.defaultHeader(), row) + cols, err := c.specs.realize(raw, defaultCJHeader, row) if err != nil { return err } @@ -67,7 +65,7 @@ func (c CronJob) Render(o interface{}, ns string, row *model1.Row) error { } // Render renders a K8s resource to screen. -func (c CronJob) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error { +func (CronJob) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error { var cj batchv1.CronJob err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cj) if err != nil { @@ -79,18 +77,18 @@ func (c CronJob) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error lastScheduled = ToAge(*cj.Status.LastScheduleTime) } - r.ID = client.MetaFQN(cj.ObjectMeta) + r.ID = client.MetaFQN(&cj.ObjectMeta) r.Fields = model1.Fields{ cj.Namespace, cj.Name, - computeVulScore(cj.ObjectMeta, &cj.Spec.JobTemplate.Spec.Template.Spec), + computeVulScore(cj.Namespace, cj.Labels, &cj.Spec.JobTemplate.Spec.Template.Spec), cj.Spec.Schedule, boolPtrToStr(cj.Spec.Suspend), strconv.Itoa(len(cj.Status.Active)), lastScheduled, - jobSelector(cj.Spec.JobTemplate.Spec), - podContainerNames(cj.Spec.JobTemplate.Spec.Template.Spec, true), - podImageNames(cj.Spec.JobTemplate.Spec.Template.Spec, true), + jobSelector(&cj.Spec.JobTemplate.Spec), + podContainerNames(&cj.Spec.JobTemplate.Spec.Template.Spec, true), + podImageNames(&cj.Spec.JobTemplate.Spec.Template.Spec, true), mapToStr(cj.Labels), "", ToAge(cj.GetCreationTimestamp()), @@ -101,7 +99,7 @@ func (c CronJob) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error // Helpers -func jobSelector(spec batchv1.JobSpec) string { +func jobSelector(spec *batchv1.JobSpec) string { if spec.Selector == nil { return MissingValue } @@ -120,31 +118,31 @@ func jobSelector(spec batchv1.JobSpec) string { return strings.Join(ss, " ") } -func podContainerNames(spec v1.PodSpec, includeInit bool) string { +func podContainerNames(spec *v1.PodSpec, includeInit bool) string { cc := make([]string, 0, len(spec.Containers)+len(spec.InitContainers)) if includeInit { - for _, c := range spec.InitContainers { - cc = append(cc, c.Name) + for i := range spec.InitContainers { + cc = append(cc, spec.InitContainers[i].Name) } } - for _, c := range spec.Containers { - cc = append(cc, c.Name) + for i := range spec.Containers { + cc = append(cc, spec.Containers[i].Name) } return strings.Join(cc, ",") } -func podImageNames(spec v1.PodSpec, includeInit bool) string { +func podImageNames(spec *v1.PodSpec, includeInit bool) string { cc := make([]string, 0, len(spec.Containers)+len(spec.InitContainers)) if includeInit { - for _, c := range spec.InitContainers { - cc = append(cc, c.Image) + for i := range spec.InitContainers { + cc = append(cc, spec.InitContainers[i].Image) } } - for _, c := range spec.Containers { - cc = append(cc, c.Image) + for i := range spec.Containers { + cc = append(cc, spec.Containers[i].Image) } return strings.Join(cc, ",") diff --git a/internal/render/cronjob_test.go b/internal/render/cronjob_test.go index 34a77a96..d15e4575 100644 --- a/internal/render/cronjob_test.go +++ b/internal/render/cronjob_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCronJobRender(t *testing.T) { c := render.CronJob{} r := model1.NewRow(6) - assert.NoError(t, c.Render(load(t, "cj"), "", &r)) + require.NoError(t, c.Render(load(t, "cj"), "", &r)) assert.Equal(t, "default/hello", r.ID) assert.Equal(t, model1.Fields{"default", "hello", "0", "*/1 * * * *", "false", "0"}, r.Fields[:6]) } diff --git a/internal/render/cust_col.go b/internal/render/cust_col.go index 2e91fae0..4f6095f2 100644 --- a/internal/render/cust_col.go +++ b/internal/render/cust_col.go @@ -14,7 +14,7 @@ import ( "k8s.io/kubectl/pkg/cmd/get" ) -var fullRX = regexp.MustCompile(`^([\w\s%\/-]+)\:?([\w\d\S\W]*?)\|?([N|T|W|S|L|R|H]{0,3})$`) +var fullRX = regexp.MustCompile(`^([\w\s%/-]+):?([\w\W]*?)\|?([NTWSLRH]{0,3})$`) type colAttr byte diff --git a/internal/render/cust_cols.go b/internal/render/cust_cols.go index ef30d478..6caa0468 100644 --- a/internal/render/cust_cols.go +++ b/internal/render/cust_cols.go @@ -4,6 +4,7 @@ package render import ( + "errors" "fmt" "log/slog" "reflect" @@ -12,6 +13,7 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/slogs" + "github.com/itchyny/gojq" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -172,6 +174,13 @@ func hydrate(o runtime.Object, cc ColumnSpecs, parsers []*jsonpath.JSONPath, rh err error ) if unstructured, ok := o.(runtime.Unstructured); ok { + if vals, ok := jqParse(cc[idx].Spec, unstructured.UnstructuredContent()); ok { + cols[idx] = RenderedCol{ + Header: cc[idx].Header, + Value: vals, + } + continue + } vals, err = parser.FindResults(unstructured.UnstructuredContent()) } else { vals, err = parser.FindResults(reflect.ValueOf(o).Elem().Interface()) @@ -232,3 +241,38 @@ func hydrate(o runtime.Object, cc ColumnSpecs, parsers []*jsonpath.JSONPath, rh return cols, nil } + +func isJQSpec(spec string) bool { + return len(strings.Split(spec, "|")) > 2 +} + +func jqParse(spec string, o map[string]any) (string, bool) { + if !isJQSpec(spec) { + return "", false + } + + exp := spec[1 : len(spec)-1] + jq, err := gojq.Parse(exp) + if err != nil { + slog.Warn("Fail to parse JQ expression", slogs.JQExp, exp, slogs.Error, err) + return "", false + } + + rr := make([]string, 0, 10) + iter := jq.Run(o) + for v, ok := iter.Next(); ok; v, ok = iter.Next() { + if e, cool := v.(error); cool && e != nil { + if errors.Is(e, new(gojq.HaltError)) { + break + } + slog.Error("JQ expression evaluation failed. Check your query", slogs.Error, e) + continue + } + rr = append(rr, fmt.Sprintf("%v", v)) + } + if len(rr) == 0 { + return "", false + } + + return strings.Join(rr, ","), true +} diff --git a/internal/render/dir.go b/internal/render/dir.go index cfaa5e14..4ba6e922 100644 --- a/internal/render/dir.go +++ b/internal/render/dir.go @@ -24,7 +24,7 @@ func (Dir) IsGeneric() bool { // ColorerFunc colors a resource row. func (Dir) ColorerFunc() model1.ColorerFunc { - return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { + return func(string, model1.Header, *model1.RowEvent) tcell.Color { return tcell.ColorCadetBlue } } @@ -32,7 +32,7 @@ func (Dir) ColorerFunc() model1.ColorerFunc { func (Dir) SetViewSetting(*config.ViewSetting) {} // Header returns a header row. -func (Dir) Header(ns string) model1.Header { +func (Dir) Header(string) model1.Header { return model1.Header{ model1.HeaderColumn{Name: "NAME"}, } @@ -40,7 +40,7 @@ func (Dir) Header(ns string) model1.Header { // Render renders a K8s resource to screen. // BOZO!! Pass in a row with pre-alloc fields?? -func (Dir) Render(o interface{}, ns string, r *model1.Row) error { +func (Dir) Render(o any, _ string, r *model1.Row) error { d, ok := o.(DirRes) if !ok { return fmt.Errorf("expected DirRes, but got %T", o) diff --git a/internal/render/dp.go b/internal/render/dp.go index 014f0bbc..845f9c13 100644 --- a/internal/render/dp.go +++ b/internal/render/dp.go @@ -17,13 +17,25 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +var defaultDPHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, + model1.HeaderColumn{Name: "READY", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "UP-TO-DATE", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "AVAILABLE", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // Deployment renders a K8s Deployment to screen. type Deployment struct { Base } // ColorerFunc colors a resource row. -func (d Deployment) ColorerFunc() model1.ColorerFunc { +func (Deployment) ColorerFunc() model1.ColorerFunc { return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { c := model1.DefaultColorer(ns, h, re) @@ -43,28 +55,14 @@ func (d Deployment) ColorerFunc() model1.ColorerFunc { // Header returns a header row. func (d Deployment) Header(_ string) model1.Header { - return d.doHeader(d.defaultHeader()) -} - -func (Deployment) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, - model1.HeaderColumn{Name: "READY", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "UP-TO-DATE", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "AVAILABLE", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return d.doHeader(defaultDPHeader) } // Render renders a K8s resource to screen. -func (d Deployment) Render(o interface{}, ns string, row *model1.Row) error { +func (d Deployment) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected Deployment, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } if err := d.defaultRow(raw, row); err != nil { return err @@ -72,9 +70,7 @@ func (d Deployment) Render(o interface{}, ns string, row *model1.Row) error { if d.specs.isEmpty() { return nil } - - // !BOZO!! Call header 2 times - cols, err := d.specs.realize(raw, d.defaultHeader(), row) + cols, err := d.specs.realize(raw, defaultDPHeader, row) if err != nil { return err } @@ -91,11 +87,11 @@ func (d Deployment) defaultRow(raw *unstructured.Unstructured, r *model1.Row) er return err } - r.ID = client.MetaFQN(dp.ObjectMeta) + r.ID = client.MetaFQN(&dp.ObjectMeta) r.Fields = model1.Fields{ dp.Namespace, dp.Name, - computeVulScore(dp.ObjectMeta, &dp.Spec.Template.Spec), + computeVulScore(dp.Namespace, dp.Labels, &dp.Spec.Template.Spec), strconv.Itoa(int(dp.Status.AvailableReplicas)) + "/" + strconv.Itoa(int(dp.Status.Replicas)), strconv.Itoa(int(dp.Status.UpdatedReplicas)), strconv.Itoa(int(dp.Status.AvailableReplicas)), diff --git a/internal/render/dp_test.go b/internal/render/dp_test.go index e4ecc4b1..52148446 100644 --- a/internal/render/dp_test.go +++ b/internal/render/dp_test.go @@ -9,25 +9,28 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDpRender(t *testing.T) { c := render.Deployment{} r := model1.NewRow(7) - assert.Nil(t, c.Render(load(t, "dp"), "", &r)) + require.NoError(t, c.Render(load(t, "dp"), "", &r)) assert.Equal(t, "icx/icx-db", r.ID) assert.Equal(t, model1.Fields{"icx", "icx-db", "0", "1/1", "1", "1"}, r.Fields[:6]) } func BenchmarkDpRender(b *testing.B) { - c := render.Deployment{} - r := model1.NewRow(7) - o := load(b, "dp") + var ( + c = render.Deployment{} + r = model1.NewRow(7) + o = load(b, "dp") + ) b.ResetTimer() b.ReportAllocs() - for i := 0; i < b.N; i++ { + for range b.N { _ = c.Render(o, "", &r) } } diff --git a/internal/render/ds.go b/internal/render/ds.go index 433b2a00..5c67ad19 100644 --- a/internal/render/ds.go +++ b/internal/render/ds.go @@ -15,6 +15,20 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +var defaultDSHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, + model1.HeaderColumn{Name: "DESIRED", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "CURRENT", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "READY", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "UP-TO-DATE", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "AVAILABLE", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // DaemonSet renders a K8s DaemonSet to screen. type DaemonSet struct { Base @@ -22,31 +36,14 @@ type DaemonSet struct { // Header returns a header row. func (d DaemonSet) Header(_ string) model1.Header { - return d.doHeader(d.defaultHeader()) -} - -// Header returns a header row. -func (DaemonSet) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, - model1.HeaderColumn{Name: "DESIRED", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "CURRENT", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "READY", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "UP-TO-DATE", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "AVAILABLE", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return d.doHeader(defaultDSHeader) } // Render renders a K8s resource to screen. -func (d DaemonSet) Render(o interface{}, ns string, row *model1.Row) error { +func (d DaemonSet) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected Deployment, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } if err := d.defaultRow(raw, row); err != nil { return err @@ -54,9 +51,7 @@ func (d DaemonSet) Render(o interface{}, ns string, row *model1.Row) error { if d.specs.isEmpty() { return nil } - - // !BOZO!! Call header 2 times - cols, err := d.specs.realize(raw, d.defaultHeader(), row) + cols, err := d.specs.realize(raw, defaultDSHeader, row) if err != nil { return err } @@ -73,11 +68,11 @@ func (d DaemonSet) defaultRow(raw *unstructured.Unstructured, r *model1.Row) err return err } - r.ID = client.MetaFQN(ds.ObjectMeta) + r.ID = client.MetaFQN(&ds.ObjectMeta) r.Fields = model1.Fields{ ds.Namespace, ds.Name, - computeVulScore(ds.ObjectMeta, &ds.Spec.Template.Spec), + computeVulScore(ds.Namespace, ds.Labels, &ds.Spec.Template.Spec), strconv.Itoa(int(ds.Status.DesiredNumberScheduled)), strconv.Itoa(int(ds.Status.CurrentNumberScheduled)), strconv.Itoa(int(ds.Status.NumberReady)), diff --git a/internal/render/ds_test.go b/internal/render/ds_test.go index 16598332..2f16869f 100644 --- a/internal/render/ds_test.go +++ b/internal/render/ds_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDaemonSetRender(t *testing.T) { c := render.DaemonSet{} r := model1.NewRow(9) - assert.NoError(t, c.Render(load(t, "ds"), "", &r)) + require.NoError(t, c.Render(load(t, "ds"), "", &r)) assert.Equal(t, "kube-system/fluentd-gcp-v3.2.0", r.ID) assert.Equal(t, model1.Fields{"kube-system", "fluentd-gcp-v3.2.0", "0", "2", "2", "2", "2", "2"}, r.Fields[:8]) } diff --git a/internal/render/ep.go b/internal/render/ep.go index 70f404ad..9377d96b 100644 --- a/internal/render/ep.go +++ b/internal/render/ep.go @@ -15,6 +15,13 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +var defaultEPHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "ENDPOINTS"}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // Endpoints renders a K8s Endpoints to screen. type Endpoints struct { Base @@ -22,28 +29,18 @@ type Endpoints struct { // Header returns a header row. func (e Endpoints) Header(_ string) model1.Header { - return e.doHeader(e.defaultHeader()) -} - -// Header returns a header row. -func (Endpoints) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "ENDPOINTS"}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return e.doHeader(defaultEPHeader) } // Render renders a K8s resource to screen. -func (e Endpoints) Render(o interface{}, ns string, row *model1.Row) error { +func (e Endpoints) Render(o any, ns string, row *model1.Row) error { if err := e.defaultRow(o, ns, row); err != nil { return err } if e.specs.isEmpty() { return nil } - cols, err := e.specs.realize(o.(*unstructured.Unstructured), e.defaultHeader(), row) + cols, err := e.specs.realize(o.(*unstructured.Unstructured), defaultEPHeader, row) if err != nil { return err } @@ -52,10 +49,10 @@ func (e Endpoints) Render(o interface{}, ns string, row *model1.Row) error { return nil } -func (e Endpoints) defaultRow(o interface{}, ns string, r *model1.Row) error { +func (e Endpoints) defaultRow(o any, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected Endpoints, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } var ep v1.Endpoints err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ep) @@ -63,7 +60,7 @@ func (e Endpoints) defaultRow(o interface{}, ns string, r *model1.Row) error { return err } - r.ID = client.MetaFQN(ep.ObjectMeta) + r.ID = client.MetaFQN(&ep.ObjectMeta) r.Fields = make(model1.Fields, 0, len(e.Header(ns))) r.Fields = model1.Fields{ ep.Namespace, @@ -91,16 +88,16 @@ func toEPs(ss []v1.EndpointSubset) string { } func portsToStrs(pp []v1.EndpointPort, ss []string) { - for i := 0; i < len(pp); i++ { + for i := range pp { ss[i] = strconv.Itoa(int(pp[i].Port)) } } -func processIPs(aa []string, pp []string, addrs []v1.EndpointAddress) { +func processIPs(aa, pp []string, addrs []v1.EndpointAddress) { const maxIPs = 3 var i int for _, a := range addrs { - if len(a.IP) == 0 { + if a.IP == "" { continue } if len(pp) == 0 { diff --git a/internal/render/ep_test.go b/internal/render/ep_test.go index f4359f3a..f9d618b3 100644 --- a/internal/render/ep_test.go +++ b/internal/render/ep_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestEndpointsRender(t *testing.T) { c := render.Endpoints{} r := model1.NewRow(4) - assert.NoError(t, c.Render(load(t, "ep"), "", &r)) + require.NoError(t, c.Render(load(t, "ep"), "", &r)) assert.Equal(t, "default/dictionary1", r.ID) assert.Equal(t, model1.Fields{"default", "dictionary1", ""}, r.Fields[:3]) } diff --git a/internal/render/generic.go b/internal/render/generic.go index bb2a5778..ed3c6695 100644 --- a/internal/render/generic.go +++ b/internal/render/generic.go @@ -11,6 +11,13 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) +var defaultGENHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // Generic renders a K8s generic resource to screen. type Generic struct { Base @@ -18,34 +25,22 @@ type Generic struct { // Header returns a header row. func (m Generic) Header(_ string) model1.Header { - return m.doHeader(m.defaultHeader()) -} - -// Header returns a header rbw. -func (Generic) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return m.doHeader(defaultGENHeader) } // Render renders a K8s resource to screen. -func (m Generic) Render(o interface{}, ns string, row *model1.Row) error { +func (m Generic) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected *Unstructured, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } - if err := m.defaultRow(raw, row); err != nil { return err } if m.specs.isEmpty() { return nil } - - cols, err := m.specs.realize(o.(*unstructured.Unstructured), m.defaultHeader(), row) + cols, err := m.specs.realize(o.(*unstructured.Unstructured), defaultGENHeader, row) if err != nil { return err } diff --git a/internal/render/helm/chart.go b/internal/render/helm/chart.go index fcfb1450..cf3adbc1 100644 --- a/internal/render/helm/chart.go +++ b/internal/render/helm/chart.go @@ -47,7 +47,7 @@ func (Chart) Header(_ string) model1.Header { } // Render renders a chart to screen. -func (c Chart) Render(o interface{}, ns string, r *model1.Row) error { +func (c Chart) Render(o any, _ string, r *model1.Row) error { h, ok := o.(ReleaseRes) if !ok { return fmt.Errorf("expected ReleaseRes, but got %T", o) @@ -68,7 +68,7 @@ func (c Chart) Render(o interface{}, ns string, r *model1.Row) error { return nil } -func (c Chart) diagnose(s string) error { +func (Chart) diagnose(s string) error { if s != "deployed" { return fmt.Errorf("chart is in an invalid state") } diff --git a/internal/render/helm/history.go b/internal/render/helm/history.go index c558e660..13d5188d 100644 --- a/internal/render/helm/history.go +++ b/internal/render/helm/history.go @@ -18,7 +18,7 @@ import ( type History struct{} // Healthy checks component health. -func (History) Healthy(ctx context.Context, o interface{}) error { +func (History) Healthy(context.Context, any) error { return nil } @@ -47,7 +47,7 @@ func (History) Header(_ string) model1.Header { } // Render renders a chart to screen. -func (c History) Render(o interface{}, ns string, r *model1.Row) error { +func (c History) Render(o any, _ string, r *model1.Row) error { h, ok := o.(ReleaseRes) if !ok { return fmt.Errorf("expected HistoryRes, but got %T", o) @@ -67,6 +67,6 @@ func (c History) Render(o interface{}, ns string, r *model1.Row) error { return nil } -func (c History) diagnose(s string) error { +func (History) diagnose(string) error { return nil } diff --git a/internal/render/helpers.go b/internal/render/helpers.go index 5ce26018..3d180869 100644 --- a/internal/render/helpers.go +++ b/internal/render/helpers.go @@ -27,15 +27,15 @@ import ( // !!BOZO!! If this has any legs?? enable scans on other container types. func ExtractImages(spec *v1.PodSpec) []string { ii := make([]string, 0, len(spec.Containers)) - for _, c := range spec.Containers { - ii = append(ii, c.Image) + for i := range spec.Containers { + ii = append(ii, spec.Containers[i].Image) } return ii } -func computeVulScore(m metav1.ObjectMeta, spec *v1.PodSpec) string { - if vul.ImgScanner == nil || vul.ImgScanner.ShouldExcludes(m) { +func computeVulScore(ns string, lbls map[string]string, spec *v1.PodSpec) string { + if vul.ImgScanner == nil || vul.ImgScanner.ShouldExcludes(ns, lbls) { return "0" } ii := ExtractImages(spec) @@ -91,26 +91,27 @@ func toSelector(m map[string]string) string { } // Blank checks if a collection is empty or all values are blank. -func blank(s []string) bool { - for _, v := range s { - if len(v) != 0 { +func blank(ss []string) bool { + for _, s := range ss { + if s != "" { return false } } + return true } // Join a slice of strings, skipping blanks. -func join(a []string, sep string) string { - switch len(a) { +func join(ss []string, sep string) string { + switch len(ss) { case 0: return "" case 1: - return a[0] + return ss[0] } - b := make([]string, 0, len(a)) - for _, s := range a { + b := make([]string, 0, len(ss)) + for _, s := range ss { if s != "" { b = append(b, s) } @@ -120,8 +121,8 @@ func join(a []string, sep string) string { } n := len(sep) * (len(b) - 1) - for i := 0; i < len(b); i++ { - n += len(a[i]) + for i := range b { + n += len(ss[i]) } var buff strings.Builder @@ -147,7 +148,7 @@ func PrintPerc(p int) string { // IntToStr converts an int to a string. func IntToStr(p int) string { - return strconv.Itoa(int(p)) + return strconv.Itoa(p) } func missing(s string) string { @@ -166,7 +167,7 @@ func na(s string) string { } func check(s, sub string) string { - if len(s) == 0 { + if s == "" { return sub } @@ -192,7 +193,7 @@ func ToAge(t metav1.Time) string { } func toAgeHuman(s string) string { - if len(s) == 0 { + if s == "" { return UnknownValue } @@ -231,12 +232,12 @@ func mapToStr(m map[string]string) string { return string(bb) } -func mapToIfc(m interface{}) (s string) { +func mapToIfc(m any) (s string) { if m == nil { return "" } - mm, ok := m.(map[string]interface{}) + mm, ok := m.(map[string]any) if !ok { return "" } diff --git a/internal/render/helpers_test.go b/internal/render/helpers_test.go index 355d4e6e..2bd2fe05 100644 --- a/internal/render/helpers_test.go +++ b/internal/render/helpers_test.go @@ -13,6 +13,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/model1" "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" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" @@ -28,11 +29,11 @@ func TestTableGenericHydrate(t *testing.T) { }, Rows: []metav1beta1.TableRow{ { - Cells: []interface{}{"fred", 10}, + Cells: []any{"fred", 10}, Object: runtime.RawExtension{Object: raw}, }, { - Cells: []interface{}{"blee", 20}, + Cells: []any{"blee", 20}, Object: runtime.RawExtension{Object: raw}, }, }, @@ -41,9 +42,9 @@ func TestTableGenericHydrate(t *testing.T) { var re Table re.SetTable("blee", &tt) - assert.Nil(t, model1.GenericHydrate("blee", &tt, rr, &re)) - assert.Equal(t, 2, len(rr)) - assert.Equal(t, 2, len(rr[0].Fields)) + require.NoError(t, model1.GenericHydrate("blee", &tt, rr, &re)) + assert.Len(t, rr, 2) + assert.Len(t, rr[0].Fields, 2) } func TestTableHydrate(t *testing.T) { @@ -52,9 +53,10 @@ func TestTableHydrate(t *testing.T) { } rr := make([]model1.Row, 1) - assert.Nil(t, model1.Hydrate("blee", oo, rr, NewPod())) - assert.Equal(t, 1, len(rr)) - assert.Equal(t, 25, len(rr[0].Fields)) + re := NewPod() + require.NoError(t, model1.Hydrate("blee", oo, rr, re)) + assert.Len(t, rr, 1) + assert.Len(t, rr[0].Fields, 25) } func TestToAge(t *testing.T) { @@ -286,7 +288,7 @@ func TestMetaFQN(t *testing.T) { for k := range uu { uc := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, uc.e, client.MetaFQN(uc.m)) + assert.Equal(t, uc.e, client.MetaFQN(&uc.m)) }) } } @@ -329,7 +331,7 @@ func BenchmarkMapToStr(b *testing.B) { b.ReportAllocs() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { mapToStr(ll) } } @@ -370,7 +372,7 @@ func BenchmarkRunesToNum(b *testing.B) { b.ReportAllocs() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { runesToNum(rr) } } @@ -423,7 +425,7 @@ func BenchmarkIntToStr(b *testing.B) { v := 10 b.ResetTimer() b.ReportAllocs() - for n := 0; n < b.N; n++ { + for range b.N { IntToStr(v) } } @@ -432,9 +434,9 @@ func BenchmarkIntToStr(b *testing.B) { func load(t *testing.T, 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 } diff --git a/internal/render/hpa.go b/internal/render/hpa.go index 44400322..65e748a1 100644 --- a/internal/render/hpa.go +++ b/internal/render/hpa.go @@ -17,7 +17,7 @@ type HorizontalPodAutoscaler struct { } // ColorerFunc colors a resource row. -func (hpa HorizontalPodAutoscaler) ColorerFunc() model1.ColorerFunc { +func (*HorizontalPodAutoscaler) ColorerFunc() model1.ColorerFunc { return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { c := model1.DefaultColorer(ns, h, re) diff --git a/internal/render/img_scan.go b/internal/render/img_scan.go index 3bbe4189..68d17e5d 100644 --- a/internal/render/img_scan.go +++ b/internal/render/img_scan.go @@ -25,7 +25,7 @@ type ImageScan struct { } // ColorerFunc colors a resource row. -func (c ImageScan) ColorerFunc() model1.ColorerFunc { +func (ImageScan) ColorerFunc() model1.ColorerFunc { return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { c := model1.DefaultColorer(ns, h, re) @@ -54,7 +54,7 @@ func (c ImageScan) ColorerFunc() model1.ColorerFunc { } // Header returns a header row. -func (ImageScan) Header(ns string) model1.Header { +func (ImageScan) Header(string) model1.Header { return model1.Header{ model1.HeaderColumn{Name: "SEVERITY"}, model1.HeaderColumn{Name: "VULNERABILITY"}, @@ -67,7 +67,7 @@ func (ImageScan) Header(ns string) model1.Header { } // Render renders a K8s resource to screen. -func (is ImageScan) Render(o interface{}, name string, r *model1.Row) error { +func (ImageScan) Render(o any, _ string, r *model1.Row) error { res, ok := o.(ImageScanRes) if !ok { return fmt.Errorf("expected ImageScanRes, but got %T", o) diff --git a/internal/render/job.go b/internal/render/job.go index d4be14e2..6778c590 100644 --- a/internal/render/job.go +++ b/internal/render/job.go @@ -17,6 +17,19 @@ import ( "k8s.io/apimachinery/pkg/util/duration" ) +var defaultJOBHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, + model1.HeaderColumn{Name: "COMPLETIONS"}, + model1.HeaderColumn{Name: "DURATION"}, + model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "CONTAINERS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "IMAGES", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // Job renders a K8s Job to screen. type Job struct { Base @@ -24,29 +37,14 @@ type Job struct { // Header returns a header row. func (j Job) Header(_ string) model1.Header { - return j.doHeader(j.defaultHeader()) -} - -func (Job) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, - model1.HeaderColumn{Name: "COMPLETIONS"}, - model1.HeaderColumn{Name: "DURATION"}, - model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "CONTAINERS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "IMAGES", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return j.doHeader(defaultJOBHeader) } // Render renders a K8s resource to screen. -func (j Job) Render(o interface{}, ns string, row *model1.Row) error { +func (j Job) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected Job, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } if err := j.defaultRow(raw, row); err != nil { return err @@ -54,8 +52,7 @@ func (j Job) Render(o interface{}, ns string, row *model1.Row) error { if j.specs.isEmpty() { return nil } - - cols, err := j.specs.realize(raw, j.defaultHeader(), row) + cols, err := j.specs.realize(raw, defaultJOBHeader, row) if err != nil { return err } @@ -70,28 +67,28 @@ func (j Job) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error { if err != nil { return err } - ready := toCompletion(job.Spec, job.Status) + ready := toCompletion(&job.Spec, &job.Status) - cc, ii := toContainers(job.Spec.Template.Spec) + cc, ii := toContainers(&job.Spec.Template.Spec) - r.ID = client.MetaFQN(job.ObjectMeta) + r.ID = client.MetaFQN(&job.ObjectMeta) r.Fields = model1.Fields{ job.Namespace, job.Name, - computeVulScore(job.ObjectMeta, &job.Spec.Template.Spec), + computeVulScore(job.Namespace, job.Labels, &job.Spec.Template.Spec), ready, - toDuration(job.Status), - jobSelector(job.Spec), + toDuration(&job.Status), + jobSelector(&job.Spec), cc, ii, - AsStatus(j.diagnose(ready, job.Status)), + AsStatus(j.diagnose(ready, &job.Status)), ToAge(job.GetCreationTimestamp()), } return nil } -func (Job) diagnose(ready string, status batchv1.JobStatus) error { +func (Job) diagnose(ready string, status *batchv1.JobStatus) error { tokens := strings.Split(ready, "/") if tokens[0] != tokens[1] && status.Failed > 0 { return fmt.Errorf("%d pods failed", status.Failed) @@ -104,7 +101,7 @@ func (Job) diagnose(ready string, status batchv1.JobStatus) error { const maxShow = 2 -func toContainers(p v1.PodSpec) (string, string) { +func toContainers(p *v1.PodSpec) (containers, images string) { cc, ii := parseContainers(p.InitContainers) cn, ci := parseContainers(p.Containers) @@ -122,15 +119,15 @@ func toContainers(p v1.PodSpec) (string, string) { } func parseContainers(cos []v1.Container) (nn, ii []string) { - for _, co := range cos { - nn = append(nn, co.Name) - ii = append(ii, co.Image) + nn, ii = make([]string, 0, len(cos)), make([]string, 0, len(cos)) + for i := range cos { + nn, ii = append(nn, cos[i].Name), append(ii, cos[i].Image) } return nn, ii } -func toCompletion(spec batchv1.JobSpec, status batchv1.JobStatus) (s string) { +func toCompletion(spec *batchv1.JobSpec, status *batchv1.JobStatus) (s string) { if spec.Completions != nil { return strconv.Itoa(int(status.Succeeded)) + "/" + strconv.Itoa(int(*spec.Completions)) } @@ -147,7 +144,7 @@ func toCompletion(spec batchv1.JobSpec, status batchv1.JobStatus) (s string) { return strconv.Itoa(int(status.Succeeded)) + "/1" } -func toDuration(status batchv1.JobStatus) string { +func toDuration(status *batchv1.JobStatus) string { if status.StartTime == nil || status.CompletionTime == nil { return MissingValue } diff --git a/internal/render/job_test.go b/internal/render/job_test.go index 028a4ddf..fc2c408f 100644 --- a/internal/render/job_test.go +++ b/internal/render/job_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestJobRender(t *testing.T) { c := render.Job{} r := model1.NewRow(4) - assert.NoError(t, c.Render(load(t, "job"), "", &r)) + require.NoError(t, c.Render(load(t, "job"), "", &r)) assert.Equal(t, "default/hello-1567179180", r.ID) assert.Equal(t, model1.Fields{"default", "hello-1567179180", "0", "1/1", "8s", "controller-uid=7473e6d0-cb3b-11e9-990f-42010a800218", "c1", "blang/busybox-bash"}, r.Fields[:8]) } diff --git a/internal/render/node.go b/internal/render/node.go index 94e2087a..fc24e739 100644 --- a/internal/render/node.go +++ b/internal/render/node.go @@ -25,6 +25,29 @@ const ( labelNodeRoleSuffix = "kubernetes.io/role" ) +var defaultNOHeader = model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "ROLE"}, + model1.HeaderColumn{Name: "ARCH", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "TAINTS"}, + model1.HeaderColumn{Name: "VERSION"}, + model1.HeaderColumn{Name: "OS-IMAGE", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "KERNEL", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "INTERNAL-IP", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "EXTERNAL-IP", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "PODS", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "CPU", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "MEM", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "%CPU", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "%MEM", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "CPU/A", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "MEM/A", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // Node renders a K8s Node to screen. type Node struct { Base @@ -32,39 +55,14 @@ type Node struct { // Header returns a header row. func (n Node) Header(_ string) model1.Header { - return n.doHeader(n.defaultHeader()) -} - -func (Node) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "STATUS"}, - model1.HeaderColumn{Name: "ROLE"}, - model1.HeaderColumn{Name: "ARCH", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "TAINTS"}, - model1.HeaderColumn{Name: "VERSION"}, - model1.HeaderColumn{Name: "OS-IMAGE", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "KERNEL", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "INTERNAL-IP", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "EXTERNAL-IP", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "PODS", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "CPU", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "MEM", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "%CPU", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "%MEM", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "CPU/A", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "MEM/A", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return n.doHeader(defaultNOHeader) } // Render renders a K8s resource to screen. -func (n Node) Render(o interface{}, ns string, row *model1.Row) error { +func (n Node) Render(o any, _ string, row *model1.Row) error { nwm, ok := o.(*NodeWithMetrics) if !ok { - return fmt.Errorf("expected PodWithMetrics, but got %T", o) + return fmt.Errorf("expected NodeWithMetrics, but got %T", o) } if err := n.defaultRow(nwm, row); err != nil { return err @@ -73,7 +71,7 @@ func (n Node) Render(o interface{}, ns string, row *model1.Row) error { return nil } - cols, err := n.specs.realize(nwm.Raw, n.defaultHeader(), row) + cols, err := n.specs.realize(nwm.Raw, defaultNOHeader, row) if err != nil { return err } @@ -168,7 +166,7 @@ type NodeWithMetrics struct { } // GetObjectKind returns a schema object. -func (n *NodeWithMetrics) GetObjectKind() schema.ObjectKind { +func (*NodeWithMetrics) GetObjectKind() schema.ObjectKind { return nil } @@ -196,7 +194,7 @@ func nodeRoles(node *v1.Node, res []string) { for k, v := range node.Labels { switch { case strings.HasPrefix(k, labelNodeRolePrefix): - if role := strings.TrimPrefix(k, labelNodeRolePrefix); len(role) > 0 { + if role := strings.TrimPrefix(k, labelNodeRolePrefix); role != "" { res[index] = role index++ } @@ -209,14 +207,14 @@ func nodeRoles(node *v1.Node, res []string) { } } - if empty(res) { + if blank(res) { res[index] = MissingValue } } func getIPs(addrs []v1.NodeAddress) (iIP, eIP string) { for _, a := range addrs { - // nolint:exhaustive + //nolint:exhaustive switch a.Type { case v1.NodeExternalIP: eIP = a.Address @@ -257,12 +255,3 @@ func status(conds []v1.NodeCondition, exempt bool, res []string) { res[index] = "SchedulingDisabled" } } - -func empty(s []string) bool { - for _, v := range s { - if len(v) != 0 { - return false - } - } - return true -} diff --git a/internal/render/node_test.go b/internal/render/node_test.go index 156b054c..50d33b60 100644 --- a/internal/render/node_test.go +++ b/internal/render/node_test.go @@ -9,6 +9,7 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) @@ -22,7 +23,7 @@ func TestNodeRender(t *testing.T) { var no render.Node r := model1.NewRow(14) err := no.Render(&pom, "", &r) - assert.Nil(t, err) + require.NoError(t, err) assert.Equal(t, "minikube", r.ID) e := model1.Fields{"minikube", "Ready", "master", "amd64", "0", "v1.15.2", "Buildroot 2018.05.3", "4.15.0", "192.168.64.107", "", "0", "10", "20", "0", "0", "4000", "7874"} @@ -30,15 +31,18 @@ func TestNodeRender(t *testing.T) { } func BenchmarkNodeRender(b *testing.B) { - pom := render.NodeWithMetrics{ - Raw: load(b, "no"), - MX: makeNodeMX("n1", "10m", "10Mi"), - } - var no render.Node - r := model1.NewRow(14) + var ( + no render.Node + r = model1.NewRow(14) + pom = render.NodeWithMetrics{ + Raw: load(b, "no"), + MX: makeNodeMX("n1", "10m", "10Mi"), + } + ) + b.ResetTimer() b.ReportAllocs() - for i := 0; i < b.N; i++ { + for range b.N { _ = no.Render(&pom, "", &r) } } diff --git a/internal/render/np.go b/internal/render/np.go index 364261e8..c54ac7f2 100644 --- a/internal/render/np.go +++ b/internal/render/np.go @@ -15,6 +15,21 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +var defaultNPHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "POD-SELECTOR"}, + model1.HeaderColumn{Name: "ING-SELECTOR", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "ING-PORTS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "ING-BLOCK", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "EGR-SELECTOR", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "EGR-PORTS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "EGR-BLOCK", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // NetworkPolicy renders a K8s NetworkPolicy to screen. type NetworkPolicy struct { Base @@ -22,31 +37,14 @@ type NetworkPolicy struct { // Header returns a header row. func (p NetworkPolicy) Header(_ string) model1.Header { - return p.doHeader(p.defaultHeader()) -} - -func (NetworkPolicy) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "POD-SELECTOR"}, - model1.HeaderColumn{Name: "ING-SELECTOR", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "ING-PORTS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "ING-BLOCK", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "EGR-SELECTOR", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "EGR-PORTS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "EGR-BLOCK", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return p.doHeader(defaultNPHeader) } // Render renders a K8s resource to screen. -func (p NetworkPolicy) Render(o interface{}, ns string, row *model1.Row) error { +func (p NetworkPolicy) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected NetworkPolicy, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } if err := p.defaultRow(raw, row); err != nil { return err @@ -55,7 +53,7 @@ func (p NetworkPolicy) Render(o interface{}, ns string, row *model1.Row) error { return nil } - cols, err := p.specs.realize(raw, p.defaultHeader(), row) + cols, err := p.specs.realize(raw, defaultNPHeader, row) if err != nil { return err } @@ -64,7 +62,7 @@ func (p NetworkPolicy) Render(o interface{}, ns string, row *model1.Row) error { return nil } -func (n NetworkPolicy) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error { +func (NetworkPolicy) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error { var np netv1.NetworkPolicy err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &np) if err != nil { @@ -81,7 +79,7 @@ func (n NetworkPolicy) defaultRow(raw *unstructured.Unstructured, r *model1.Row) if len(np.Spec.PodSelector.MatchExpressions) > 0 { podSel += "::" + expToStr(np.Spec.PodSelector.MatchExpressions) } - r.ID = client.MetaFQN(np.ObjectMeta) + r.ID = client.MetaFQN(&np.ObjectMeta) r.Fields = model1.Fields{ np.Namespace, np.Name, @@ -102,7 +100,7 @@ func (n NetworkPolicy) defaultRow(raw *unstructured.Unstructured, r *model1.Row) // Helpers... -func ingress(ii []netv1.NetworkPolicyIngressRule) (string, string, string) { +func ingress(ii []netv1.NetworkPolicyIngressRule) (port, selector, block string) { var ports, sels, blocks []string for _, i := range ii { if p := portsToStr(i.Ports); p != "" { @@ -119,7 +117,7 @@ func ingress(ii []netv1.NetworkPolicyIngressRule) (string, string, string) { return strings.Join(ports, ","), strings.Join(sels, ","), strings.Join(blocks, ",") } -func egress(ee []netv1.NetworkPolicyEgressRule) (string, string, string) { +func egress(ee []netv1.NetworkPolicyEgressRule) (port, selector, block string) { var ports, sels, blocks []string for _, e := range ee { if p := portsToStr(e.Ports); p != "" { @@ -151,7 +149,7 @@ func portsToStr(pp []netv1.NetworkPolicyPort) string { return strings.Join(ports, ",") } -func peersToStr(pp []netv1.NetworkPolicyPeer) (string, string) { +func peersToStr(pp []netv1.NetworkPolicyPeer) (selector, ip string) { sels := make([]string, 0, len(pp)) ips := make([]string, 0, len(pp)) for _, p := range pp { diff --git a/internal/render/np_test.go b/internal/render/np_test.go index 28942782..b2839d27 100644 --- a/internal/render/np_test.go +++ b/internal/render/np_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNetworkPolicyRender(t *testing.T) { c := render.NetworkPolicy{} r := model1.NewRow(9) - assert.NoError(t, c.Render(load(t, "np"), "", &r)) + require.NoError(t, c.Render(load(t, "np"), "", &r)) assert.Equal(t, "default/fred", r.ID) assert.Equal(t, model1.Fields{"default", "fred", "app=nginx", "ns:app=blee,po:app=fred", "TCP:6379", "172.17.0.0/16[172.17.1.0/24,172.17.3.0/24...]", "", "TCP:5978", "10.0.0.0/24"}, r.Fields[:9]) } diff --git a/internal/render/ns.go b/internal/render/ns.go index 6438c945..e0fd0f29 100644 --- a/internal/render/ns.go +++ b/internal/render/ns.go @@ -16,13 +16,21 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +var defaultNSHeader = model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // Namespace renders a K8s Namespace to screen. type Namespace struct { Base } // ColorerFunc colors a resource row. -func (n Namespace) ColorerFunc() model1.ColorerFunc { +func (Namespace) ColorerFunc() model1.ColorerFunc { return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { c := model1.DefaultColorer(ns, h, re) if c == model1.ErrColor { @@ -41,24 +49,14 @@ func (n Namespace) ColorerFunc() model1.ColorerFunc { // Header returns a header row. func (n Namespace) Header(_ string) model1.Header { - return n.doHeader(n.defaultHeader()) -} - -func (Namespace) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "STATUS"}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return n.doHeader(defaultNSHeader) } // Render renders a K8s resource to screen. -func (n Namespace) Render(o interface{}, ns string, row *model1.Row) error { +func (n Namespace) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected NetworkPolicy, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } if err := n.defaultRow(raw, row); err != nil { return err @@ -67,7 +65,7 @@ func (n Namespace) Render(o interface{}, ns string, row *model1.Row) error { return nil } - cols, err := n.specs.realize(raw, n.defaultHeader(), row) + cols, err := n.specs.realize(raw, defaultNSHeader, row) if err != nil { return err } @@ -83,7 +81,7 @@ func (n Namespace) defaultRow(raw *unstructured.Unstructured, r *model1.Row) err return err } - r.ID = client.MetaFQN(ns.ObjectMeta) + r.ID = client.MetaFQN(&ns.ObjectMeta) r.Fields = model1.Fields{ ns.Name, string(ns.Status.Phase), diff --git a/internal/render/ns_test.go b/internal/render/ns_test.go index ad4337bd..8f541b28 100644 --- a/internal/render/ns_test.go +++ b/internal/render/ns_test.go @@ -10,6 +10,7 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNSColorer(t *testing.T) { @@ -73,7 +74,7 @@ func TestNamespaceRender(t *testing.T) { c := render.Namespace{} r := model1.NewRow(3) - assert.NoError(t, c.Render(load(t, "ns"), "-", &r)) + require.NoError(t, c.Render(load(t, "ns"), "-", &r)) assert.Equal(t, "-/kube-system", r.ID) assert.Equal(t, model1.Fields{"kube-system", "Active"}, r.Fields[:2]) } diff --git a/internal/render/pdb.go b/internal/render/pdb.go index f5ff6bc5..35704f60 100644 --- a/internal/render/pdb.go +++ b/internal/render/pdb.go @@ -16,6 +16,20 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" ) +var defaultPDBHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "MIN-AVAILABLE", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "MAX-UNAVAILABLE", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "ALLOWED-DISRUPTIONS", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "CURRENT", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "DESIRED", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "EXPECTED", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // PodDisruptionBudget renders a K8s PodDisruptionBudget to screen. type PodDisruptionBudget struct { Base @@ -23,30 +37,14 @@ type PodDisruptionBudget struct { // Header returns a header row. func (p PodDisruptionBudget) Header(_ string) model1.Header { - return p.doHeader(p.defaultHeader()) -} - -func (PodDisruptionBudget) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "MIN-AVAILABLE", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "MAX-UNAVAILABLE", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "ALLOWED-DISRUPTIONS", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "CURRENT", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "DESIRED", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "EXPECTED", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return p.doHeader(defaultPDBHeader) } // Render renders a K8s resource to screen. -func (p PodDisruptionBudget) Render(o interface{}, ns string, row *model1.Row) error { +func (p PodDisruptionBudget) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected PodDisruptionBudget, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } if err := p.defaultRow(raw, row); err != nil { return err @@ -55,7 +53,7 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, row *model1.Row) e return nil } - cols, err := p.specs.realize(raw, p.defaultHeader(), row) + cols, err := p.specs.realize(raw, defaultPDBHeader, row) if err != nil { return err } @@ -71,7 +69,7 @@ func (p PodDisruptionBudget) defaultRow(raw *unstructured.Unstructured, r *model return err } - r.ID = client.MetaFQN(pdb.ObjectMeta) + r.ID = client.MetaFQN(&pdb.ObjectMeta) r.Fields = model1.Fields{ pdb.Namespace, pdb.Name, @@ -89,13 +87,14 @@ func (p PodDisruptionBudget) defaultRow(raw *unstructured.Unstructured, r *model return nil } -func (PodDisruptionBudget) diagnose(min *intstr.IntOrString, healthy int32) error { - if min == nil { +func (PodDisruptionBudget) diagnose(v *intstr.IntOrString, healthy int32) error { + if v == nil { return nil } - if min.IntVal > healthy { - return fmt.Errorf("expected %d but got %d", min.IntVal, healthy) + if v.IntVal > healthy { + return fmt.Errorf("expected %d but got %d", v.IntVal, healthy) } + return nil } diff --git a/internal/render/pdb_test.go b/internal/render/pdb_test.go index 9567c487..757eedae 100644 --- a/internal/render/pdb_test.go +++ b/internal/render/pdb_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPodDisruptionBudgetRender(t *testing.T) { c := render.PodDisruptionBudget{} r := model1.NewRow(9) - assert.NoError(t, c.Render(load(t, "pdb"), "", &r)) + require.NoError(t, c.Render(load(t, "pdb"), "", &r)) assert.Equal(t, "default/fred", r.ID) assert.Equal(t, model1.Fields{"default", "fred", "2", render.NAValue, "0", "0", "2", "0"}, r.Fields[:8]) } diff --git a/internal/render/pod.go b/internal/render/pod.go index 6e93b1d2..0c8b0d2d 100644 --- a/internal/render/pod.go +++ b/internal/render/pod.go @@ -7,6 +7,7 @@ import ( "fmt" "strconv" "strings" + "time" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/model1" @@ -18,6 +19,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/cache" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) @@ -46,20 +48,55 @@ const ( PhaseEvicted = "Evicted" ) +var defaultPodHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, + model1.HeaderColumn{Name: "PF"}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "RESTARTS", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "LAST RESTART", Attrs: model1.Attrs{Align: tview.AlignRight, Time: true, Wide: true}}, + model1.HeaderColumn{Name: "CPU", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "MEM", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "CPU/RL", Attrs: model1.Attrs{Align: tview.AlignRight, Wide: true}}, + model1.HeaderColumn{Name: "MEM/RL", Attrs: model1.Attrs{Align: tview.AlignRight, Wide: true}}, + model1.HeaderColumn{Name: "%CPU/R", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "%CPU/L", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "%MEM/R", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "%MEM/L", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, + model1.HeaderColumn{Name: "IP"}, + model1.HeaderColumn{Name: "NODE"}, + model1.HeaderColumn{Name: "SERVICE-ACCOUNT", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "NOMINATED NODE", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "READINESS GATES", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "QOS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + +const ( + cacheSize = 5_000 + expiration = 5 * time.Minute +) + // Pod renders a K8s Pod to screen. type Pod struct { *Base + cache *cache.LRUExpireCache } // NewPod returns a new instance. func NewPod() *Pod { return &Pod{ - Base: new(Base), + Base: new(Base), + cache: cache.NewLRUExpireCache(cacheSize), } } // ColorerFunc colors a resource row. -func (p Pod) ColorerFunc() model1.ColorerFunc { +func (*Pod) ColorerFunc() model1.ColorerFunc { return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { c := model1.DefaultColorer(ns, h, re) @@ -90,42 +127,12 @@ func (p Pod) ColorerFunc() model1.ColorerFunc { } // Header returns a header row. -func (p Pod) Header(_ string) model1.Header { - return p.doHeader(p.defaultHeader()) -} - -func (Pod) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, - model1.HeaderColumn{Name: "PF"}, - model1.HeaderColumn{Name: "READY"}, - model1.HeaderColumn{Name: "STATUS"}, - model1.HeaderColumn{Name: "RESTARTS", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "LAST RESTART", Attrs: model1.Attrs{Align: tview.AlignRight, Time: true, Wide: true}}, - model1.HeaderColumn{Name: "CPU", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "MEM", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "CPU/RL", Attrs: model1.Attrs{Align: tview.AlignRight, Wide: true}}, - model1.HeaderColumn{Name: "MEM/RL", Attrs: model1.Attrs{Align: tview.AlignRight, Wide: true}}, - model1.HeaderColumn{Name: "%CPU/R", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "%CPU/L", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "%MEM/R", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "%MEM/L", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}}, - model1.HeaderColumn{Name: "IP"}, - model1.HeaderColumn{Name: "NODE"}, - model1.HeaderColumn{Name: "SERVICE-ACCOUNT", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "NOMINATED NODE", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "READINESS GATES", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "QOS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } +func (p *Pod) Header(string) model1.Header { + return p.doHeader(defaultPodHeader) } // Render renders a K8s resource to screen. -func (p Pod) Render(o interface{}, ns string, row *model1.Row) error { +func (p *Pod) Render(o any, _ string, row *model1.Row) error { pwm, ok := o.(*PodWithMetrics) if !ok { return fmt.Errorf("expected PodWithMetrics, but got %T", o) @@ -136,9 +143,7 @@ func (p Pod) Render(o interface{}, ns string, row *model1.Row) error { if p.specs.isEmpty() { return nil } - - // !BOZO!! Call header 2 times - cols, err := p.specs.realize(pwm.Raw, p.defaultHeader(), row) + cols, err := p.specs.realize(pwm.Raw, defaultPodHeader, row) if err != nil { return err } @@ -147,32 +152,49 @@ func (p Pod) Render(o interface{}, ns string, row *model1.Row) error { return nil } -func (p Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error { - var po v1.Pod - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object, &po); err != nil { +func (p *Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error { + var st v1.PodStatus + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object["status"].(map[string]any), &st); err != nil { return err } + key := pwm.Raw.GetUID() + for _, o := range pwm.Raw.GetOwnerReferences() { + if o.Controller != nil && *o.Controller { + key = o.UID + break + } + } - ics := po.Status.InitContainerStatuses - _, _, irc := p.Statuses(ics) - cs := po.Status.ContainerStatuses - cr, _, rc := p.Statuses(cs) - lr := p.lastRestart(cs) + spec := new(v1.PodSpec) + if cspec, ok := p.cache.Get(key); ok { + spec = cspec.(*v1.PodSpec) + } else { + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object["spec"].(map[string]any), spec); err != nil { + return err + } + p.cache.Add(key, spec, expiration) + } + + dt := pwm.Raw.GetDeletionTimestamp() + _, _, irc, _ := p.Statuses(st.InitContainerStatuses) + cr, _, rc, lr := p.Statuses(st.ContainerStatuses) var ccmx []mv1beta1.ContainerMetrics if pwm.MX != nil { ccmx = pwm.MX.Containers } - c, r := gatherCoMX(&po.Spec, ccmx) - phase := p.Phase(&po) + c, r := gatherCoMX(spec, ccmx) + phase := p.Phase(dt, spec, &st) - row.ID = client.MetaFQN(po.ObjectMeta) + ns, n := pwm.Raw.GetNamespace(), pwm.Raw.GetName() + + row.ID = client.FQN(ns, n) row.Fields = model1.Fields{ - po.Namespace, - po.Name, - computeVulScore(po.ObjectMeta, &po.Spec), + ns, + n, + computeVulScore(ns, pwm.Raw.GetLabels(), spec), "โ—", - strconv.Itoa(cr) + "/" + strconv.Itoa(len(po.Spec.Containers)), + strconv.Itoa(cr) + "/" + strconv.Itoa(len(spec.Containers)), phase, strconv.Itoa(rc + irc), ToAge(lr), @@ -184,21 +206,21 @@ func (p Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error { client.ToPercentageStr(c.cpu, r.lcpu), client.ToPercentageStr(c.mem, r.mem), client.ToPercentageStr(c.mem, r.lmem), - na(po.Status.PodIP), - na(po.Spec.NodeName), - na(po.Spec.ServiceAccountName), - asNominated(po.Status.NominatedNodeName), - asReadinessGate(po), - p.mapQOS(po.Status.QOSClass), - mapToStr(po.Labels), - AsStatus(p.diagnose(phase, cr, len(cs))), - ToAge(po.GetCreationTimestamp()), + na(st.PodIP), + na(spec.NodeName), + na(spec.ServiceAccountName), + asNominated(st.NominatedNodeName), + asReadinessGate(spec, &st), + p.mapQOS(st.QOSClass), + mapToStr(pwm.Raw.GetLabels()), + AsStatus(p.diagnose(phase, cr, len(st.ContainerStatuses))), + ToAge(pwm.Raw.GetCreationTimestamp()), } return nil } -func (p Pod) diagnose(phase string, cr, ct int) error { +func (*Pod) diagnose(phase string, cr, ct int) error { if phase == Completed { return nil } @@ -222,15 +244,15 @@ func asNominated(n string) string { return n } -func asReadinessGate(pod v1.Pod) string { - if len(pod.Spec.ReadinessGates) == 0 { +func asReadinessGate(spec *v1.PodSpec, st *v1.PodStatus) string { + if len(spec.ReadinessGates) == 0 { return MissingValue } var trueConditions int - for _, readinessGate := range pod.Spec.ReadinessGates { + for _, readinessGate := range spec.ReadinessGates { conditionType := readinessGate.ConditionType - for _, condition := range pod.Status.Conditions { + for _, condition := range st.Conditions { if condition.Type == conditionType { if condition.Status == "True" { trueConditions++ @@ -240,7 +262,7 @@ func asReadinessGate(pod v1.Pod) string { } } - return strconv.Itoa(trueConditions) + "/" + strconv.Itoa(len(pod.Spec.ReadinessGates)) + return strconv.Itoa(trueConditions) + "/" + strconv.Itoa(len(spec.ReadinessGates)) } // PodWithMetrics represents a pod and its metrics. @@ -250,7 +272,7 @@ type PodWithMetrics struct { } // GetObjectKind returns a schema object. -func (p *PodWithMetrics) GetObjectKind() schema.ObjectKind { +func (*PodWithMetrics) GetObjectKind() schema.ObjectKind { return nil } @@ -276,10 +298,10 @@ func gatherCoMX(spec *v1.PodSpec, ccmx []mv1beta1.ContainerMetrics) (c, r metric return } -func cosLimits(cc []v1.Container) (resource.Quantity, resource.Quantity) { +func cosLimits(cc []v1.Container) (cpuQ, memQ resource.Quantity) { cpu, mem := new(resource.Quantity), new(resource.Quantity) - for _, c := range cc { - limits := c.Resources.Limits + for i := range cc { + limits := cc[i].Resources.Limits if len(limits) == 0 { continue } @@ -294,10 +316,10 @@ func cosLimits(cc []v1.Container) (resource.Quantity, resource.Quantity) { return *cpu, *mem } -func cosRequests(cc []v1.Container) (resource.Quantity, resource.Quantity) { +func cosRequests(cc []v1.Container) (cpuQ, memQ resource.Quantity) { cpu, mem := new(resource.Quantity), new(resource.Quantity) - for _, c := range cc { - co := c + for i := range cc { + co := cc[i] rl := containerRequests(&co) if rl.Cpu() != nil { cpu.Add(*rl.Cpu()) @@ -310,7 +332,7 @@ func cosRequests(cc []v1.Container) (resource.Quantity, resource.Quantity) { return *cpu, *mem } -func currentRes(ccmx []mv1beta1.ContainerMetrics) (resource.Quantity, resource.Quantity) { +func currentRes(ccmx []mv1beta1.ContainerMetrics) (cpuQ, memQ resource.Quantity) { cpu, mem := new(resource.Quantity), new(resource.Quantity) if ccmx == nil { return *cpu, *mem @@ -325,7 +347,7 @@ func currentRes(ccmx []mv1beta1.ContainerMetrics) (resource.Quantity, resource.Q } func (*Pod) mapQOS(class v1.PodQOSClass) string { - // nolint:exhaustive + //nolint:exhaustive switch class { case v1.PodQOSGuaranteed: return "GA" @@ -337,61 +359,54 @@ func (*Pod) mapQOS(class v1.PodQOSClass) string { } // Statuses reports current pod container statuses. -func (*Pod) Statuses(ss []v1.ContainerStatus) (cr, ct, rc int) { - for _, c := range ss { - if c.State.Terminated != nil { +func (*Pod) Statuses(cc []v1.ContainerStatus) (cr, ct, rc int, latest metav1.Time) { + for i := range cc { + if cc[i].State.Terminated != nil { ct++ } - if c.Ready { - cr = cr + 1 + if cc[i].Ready { + cr++ + } + rc += int(cc[i].RestartCount) + + if t := cc[i].LastTerminationState.Terminated; t != nil { + ts := cc[i].LastTerminationState.Terminated.FinishedAt + if latest.IsZero() || ts.After(latest.Time) { + latest = ts + } } - rc += int(c.RestartCount) } return } -// lastRestart returns the last container restart time. -func (*Pod) lastRestart(ss []v1.ContainerStatus) (latest metav1.Time) { - for _, c := range ss { - if c.LastTerminationState.Terminated == nil { - continue - } - ts := c.LastTerminationState.Terminated.FinishedAt - if latest.IsZero() || ts.After(latest.Time) { - latest = ts - } - } - return -} - // Phase reports the given pod phase. -func (p *Pod) Phase(po *v1.Pod) string { - status := string(po.Status.Phase) - if po.Status.Reason != "" { - if po.DeletionTimestamp != nil && po.Status.Reason == NodeUnreachablePodReason { +func (p *Pod) Phase(dt *metav1.Time, spec *v1.PodSpec, st *v1.PodStatus) string { + status := string(st.Phase) + if st.Reason != "" { + if dt != nil && st.Reason == NodeUnreachablePodReason { return "Unknown" } - status = po.Status.Reason + status = st.Reason } - status, ok := p.initContainerPhase(po, status) + status, ok := p.initContainerPhase(spec, st, status) if ok { return status } - status, ok = p.containerPhase(po.Status, status) + status, ok = p.containerPhase(st, status) if ok && status == Completed { status = Running } - if po.DeletionTimestamp == nil { + if dt == nil { return status } return Terminating } -func (*Pod) containerPhase(st v1.PodStatus, status string) (string, bool) { +func (*Pod) containerPhase(st *v1.PodStatus, status string) (string, bool) { var running bool for i := len(st.ContainerStatuses) - 1; i >= 0; i-- { cs := st.ContainerStatuses[i] @@ -414,14 +429,15 @@ func (*Pod) containerPhase(st v1.PodStatus, status string) (string, bool) { return status, running } -func (*Pod) initContainerPhase(po *v1.Pod, status string) (string, bool) { - count := len(po.Spec.InitContainers) +func (*Pod) initContainerPhase(spec *v1.PodSpec, pst *v1.PodStatus, status string) (string, bool) { + count := len(spec.InitContainers) rs := make(map[string]bool, count) - for _, c := range po.Spec.InitContainers { - rs[c.Name] = restartableInitCO(c.RestartPolicy) + for i := range spec.InitContainers { + co := spec.InitContainers[i] + rs[co.Name] = restartableInitCO(co.RestartPolicy) } - for i, cs := range po.Status.InitContainerStatuses { - if s := checkInitContainerStatus(cs, i, count, rs[cs.Name]); s != "" { + for i := range pst.InitContainerStatuses { + if s := checkInitContainerStatus(&pst.InitContainerStatuses[i], i, count, rs[pst.InitContainerStatuses[i].Name]); s != "" { return s, true } } @@ -432,7 +448,7 @@ func (*Pod) initContainerPhase(po *v1.Pod, status string) (string, bool) { // ---------------------------------------------------------------------------- // Helpers.. -func checkInitContainerStatus(cs v1.ContainerStatus, count, initCount int, restartable bool) string { +func checkInitContainerStatus(cs *v1.ContainerStatus, count, initCount int, restartable bool) string { switch { case cs.State.Terminated != nil: if cs.State.Terminated.ExitCode == 0 { @@ -486,7 +502,7 @@ func PodStatus(pod *v1.Pod) string { reason = "Init:" + container.State.Terminated.Reason } initializing = true - case container.State.Waiting != nil && len(container.State.Waiting.Reason) > 0 && container.State.Waiting.Reason != "PodInitializing": + case container.State.Waiting != nil && container.State.Waiting.Reason != "" && container.State.Waiting.Reason != "PodInitializing": reason = "Init:" + container.State.Waiting.Reason initializing = true default: @@ -499,17 +515,18 @@ func PodStatus(pod *v1.Pod) string { var hasRunning bool for i := len(pod.Status.ContainerStatuses) - 1; i >= 0; i-- { container := pod.Status.ContainerStatuses[i] - if container.State.Waiting != nil && container.State.Waiting.Reason != "" { + switch { + case container.State.Waiting != nil && container.State.Waiting.Reason != "": reason = container.State.Waiting.Reason - } else if container.State.Terminated != nil && container.State.Terminated.Reason != "" { + case container.State.Terminated != nil && container.State.Terminated.Reason != "": reason = container.State.Terminated.Reason - } else if container.State.Terminated != nil && container.State.Terminated.Reason == "" { + case container.State.Terminated != nil && container.State.Terminated.Reason == "": if container.State.Terminated.Signal != 0 { reason = fmt.Sprintf("Signal:%d", container.State.Terminated.Signal) } else { reason = fmt.Sprintf("ExitCode:%d", container.State.Terminated.ExitCode) } - } else if container.Ready && container.State.Running != nil { + case container.Ready && container.State.Running != nil: hasRunning = true } } @@ -548,7 +565,8 @@ func restartableInitCO(p *v1.ContainerRestartPolicy) bool { func filterSidecarCO(cc []v1.Container) []v1.Container { rcc := make([]v1.Container, 0, len(cc)) - for _, c := range cc { + for i := range cc { + c := cc[i] if c.RestartPolicy != nil && *c.RestartPolicy == v1.ContainerRestartPolicyAlways { rcc = append(rcc, c) } diff --git a/internal/render/pod_int_test.go b/internal/render/pod_int_test.go index d135aa5d..e9215318 100644 --- a/internal/render/pod_int_test.go +++ b/internal/render/pod_int_test.go @@ -17,7 +17,7 @@ import ( ) func Test_checkInitContainerStatus(t *testing.T) { - true := true + trueVal := true uu := map[string]struct { status v1.ContainerStatus e string @@ -30,7 +30,7 @@ func Test_checkInitContainerStatus(t *testing.T) { "restart": { status: v1.ContainerStatus{ Name: "ic1", - Started: &true, + Started: &trueVal, State: v1.ContainerState{}, }, restart: true, @@ -39,7 +39,7 @@ func Test_checkInitContainerStatus(t *testing.T) { "no-restart": { status: v1.ContainerStatus{ Name: "ic1", - Started: &true, + Started: &trueVal, State: v1.ContainerState{}, }, e: "Init:0/0", @@ -125,7 +125,7 @@ func Test_checkInitContainerStatus(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, checkInitContainerStatus(u.status, u.count, u.total, u.restart)) + assert.Equal(t, u.e, checkInitContainerStatus(&u.status, u.count, u.total, u.restart)) }) } } @@ -267,7 +267,7 @@ func Test_containerPhase(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - s, ok := p.containerPhase(u.status, "") + s, ok := p.containerPhase(&u.status, "") assert.Equal(t, u.ok, ok) assert.Equal(t, u.e, s) }) @@ -427,7 +427,8 @@ func Test_lastRestart(t *testing.T) { var p Pod for name, u := range uu { t.Run(name, func(t *testing.T) { - assert.Equal(t, u.expected, p.lastRestart(u.containerStatuses)) + _, _, _, lr := p.Statuses(u.containerStatuses) + assert.Equal(t, u.expected, lr) }) } } @@ -618,7 +619,7 @@ func makeRes(c, m string) v1.ResourceList { } } -func makeCoMX(n string, c, m string) mv1beta1.ContainerMetrics { +func makeCoMX(n, c, m string) mv1beta1.ContainerMetrics { return mv1beta1.ContainerMetrics{ Name: n, Usage: makeRes(c, m), diff --git a/internal/render/pod_test.go b/internal/render/pod_test.go index 2c9c96c6..6da164dd 100644 --- a/internal/render/pod_test.go +++ b/internal/render/pod_test.go @@ -10,6 +10,7 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" res "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -160,7 +161,7 @@ func TestPodRender(t *testing.T) { po := render.NewPod() r := model1.NewRow(14) err := po.Render(&pom, "", &r) - assert.Nil(t, err) + require.NoError(t, err) assert.Equal(t, "default/nginx", r.ID) e := model1.Fields{"default", "nginx", "0", "โ—", "1/1", "Running", "0", "", "100", "50", "100:0", "70:170", "100", "n/a", "71", "29", "172.17.0.6", "minikube", "default", ""} @@ -177,7 +178,7 @@ func BenchmarkPodRender(b *testing.B) { b.ReportAllocs() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { _ = po.Render(&pom, "", &r) } } @@ -191,7 +192,7 @@ func TestPodInitRender(t *testing.T) { po := render.NewPod() r := model1.NewRow(14) err := po.Render(&pom, "", &r) - assert.Nil(t, err) + require.NoError(t, err) assert.Equal(t, "default/nginx", r.ID) e := model1.Fields{"default", "nginx", "0", "โ—", "1/1", "Init:0/1", "0", "", "10", "10", "100:0", "70:170", "10", "n/a", "14", "5", "172.17.0.6", "minikube", "default", ""} @@ -207,7 +208,7 @@ func TestPodSidecarRender(t *testing.T) { po := render.NewPod() r := model1.NewRow(14) err := po.Render(&pom, "", &r) - assert.Nil(t, err) + require.NoError(t, err) assert.Equal(t, "default/sleep", r.ID) e := model1.Fields{"default", "sleep", "0", "โ—", "1/1", "Running", "0", "", "100", "40", "50:250", "50:80", "200", "40", "80", "50", "10.244.0.8", "kind-control-plane", "default", ""} @@ -655,7 +656,7 @@ func TestCheckPhase(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, p.Phase(&u.pod)) + assert.Equal(t, u.e, p.Phase(u.pod.DeletionTimestamp, &u.pod.Spec, &u.pod.Status)) }) } } diff --git a/internal/render/policy.go b/internal/render/policy.go index 2e8297a6..1b809943 100644 --- a/internal/render/policy.go +++ b/internal/render/policy.go @@ -37,13 +37,13 @@ type Policy struct { // ColorerFunc colors a resource row. func (Policy) ColorerFunc() model1.ColorerFunc { - return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { + return func(string, model1.Header, *model1.RowEvent) tcell.Color { return tcell.ColorMediumSpringGreen } } // Header returns a header row. -func (Policy) Header(ns string) model1.Header { +func (Policy) Header(string) model1.Header { h := model1.Header{ model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAME"}, @@ -57,8 +57,8 @@ func (Policy) Header(ns string) model1.Header { } // Render renders a K8s resource to screen. -func (Policy) Render(o interface{}, gvr string, r *model1.Row) error { - p, ok := o.(PolicyRes) +func (Policy) Render(o any, _ string, r *model1.Row) error { + p, ok := o.(*PolicyRes) if !ok { return fmt.Errorf("expecting PolicyRes but got %T", o) } @@ -102,8 +102,8 @@ type PolicyRes struct { } // NewPolicyRes returns a new policy. -func NewPolicyRes(ns, binding, res, grp string, vv []string) PolicyRes { - return PolicyRes{ +func NewPolicyRes(ns, binding, res, grp string, vv []string) *PolicyRes { + return &PolicyRes{ Namespace: ns, Binding: binding, Resource: res, @@ -113,13 +113,14 @@ func NewPolicyRes(ns, binding, res, grp string, vv []string) PolicyRes { } // GR returns the group/resource path. -func (p PolicyRes) GR() string { +func (p *PolicyRes) GR() string { return p.Group + "/" + p.Resource } -func (p PolicyRes) Merge(p1 PolicyRes) (PolicyRes, error) { +// Merge merges two policies. +func (p *PolicyRes) Merge(p1 *PolicyRes) (*PolicyRes, error) { if p.GR() != p1.GR() { - return PolicyRes{}, fmt.Errorf("policy mismatch %s vs %s", p.GR(), p1.GR()) + return nil, fmt.Errorf("policy mismatch %s vs %s", p.GR(), p1.GR()) } for _, v := range p1.Verbs { @@ -131,7 +132,7 @@ func (p PolicyRes) Merge(p1 PolicyRes) (PolicyRes, error) { return p, nil } -func (p PolicyRes) hasVerb(v1 string) bool { +func (p *PolicyRes) hasVerb(v1 string) bool { for _, v := range p.Verbs { if v == v1 { return true @@ -142,20 +143,20 @@ func (p PolicyRes) hasVerb(v1 string) bool { } // GetObjectKind returns a schema object. -func (p PolicyRes) GetObjectKind() schema.ObjectKind { +func (*PolicyRes) GetObjectKind() schema.ObjectKind { return nil } // DeepCopyObject returns a container copy. -func (p PolicyRes) DeepCopyObject() runtime.Object { +func (p *PolicyRes) DeepCopyObject() runtime.Object { return p } // Policies represents a collection of RBAC policies. -type Policies []PolicyRes +type Policies []*PolicyRes // Upsert adds a new policy. -func (pp Policies) Upsert(p PolicyRes) Policies { +func (pp Policies) Upsert(p *PolicyRes) Policies { idx, ok := pp.find(p.GR()) if !ok { return append(pp, p) diff --git a/internal/render/policy_test.go b/internal/render/policy_test.go index b0770d14..af5e2fb6 100644 --- a/internal/render/policy_test.go +++ b/internal/render/policy_test.go @@ -10,11 +10,12 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPolicyResMerge(t *testing.T) { uu := map[string]struct { - p1, p2, e render.PolicyRes + p1, p2, e *render.PolicyRes err error }{ "simple": { @@ -58,7 +59,7 @@ func TestPolicyRender(t *testing.T) { Verbs: []string{"get", "list", "watch"}, } - assert.Nil(t, p.Render(o, "fred", &r)) + require.NoError(t, p.Render(&o, "fred", &r)) assert.Equal(t, "blee/res", r.ID) assert.Equal(t, model1.Fields{ "blee", diff --git a/internal/render/port_forward_test.go b/internal/render/port_forward_test.go index 20e0683a..e5449f95 100644 --- a/internal/render/port_forward_test.go +++ b/internal/render/port_forward_test.go @@ -10,6 +10,7 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPortForwardRender(t *testing.T) { @@ -25,7 +26,7 @@ func TestPortForwardRender(t *testing.T) { var p render.PortForward var r model1.Row - assert.Nil(t, p.Render(o, "fred", &r)) + require.NoError(t, p.Render(o, "fred", &r)) assert.Equal(t, "blee/fred", r.ID) assert.Equal(t, model1.Fields{ "blee", @@ -43,30 +44,30 @@ func TestPortForwardRender(t *testing.T) { type fwd struct{} -func (f fwd) ID() string { +func (fwd) ID() string { return "blee/fred" } -func (f fwd) Path() string { +func (fwd) Path() string { return "blee/fred" } -func (f fwd) Container() string { +func (fwd) Container() string { return "co" } -func (f fwd) Port() string { +func (fwd) Port() string { return "p1:p2" } -func (f fwd) Active() bool { +func (fwd) Active() bool { return true } -func (f fwd) Age() time.Time { +func (fwd) Age() time.Time { return testTime() } -func (f fwd) Address() string { +func (fwd) Address() string { return "" } diff --git a/internal/render/portforward.go b/internal/render/portforward.go index c128c2f2..dfb5e3e7 100644 --- a/internal/render/portforward.go +++ b/internal/render/portforward.go @@ -44,13 +44,13 @@ type PortForward struct { // ColorerFunc colors a resource row. func (PortForward) ColorerFunc() model1.ColorerFunc { - return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { + return func(string, model1.Header, *model1.RowEvent) tcell.Color { return tcell.ColorSkyblue } } // Header returns a header row. -func (PortForward) Header(ns string) model1.Header { +func (PortForward) Header(string) model1.Header { return model1.Header{ model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAME"}, @@ -65,7 +65,7 @@ func (PortForward) Header(ns string) model1.Header { } // Render renders a K8s resource to screen. -func (f PortForward) Render(o interface{}, gvr string, r *model1.Row) error { +func (PortForward) Render(o any, _ string, r *model1.Row) error { pf, ok := o.(ForwardRes) if !ok { return fmt.Errorf("expecting a ForwardRes but got %T", o) @@ -127,7 +127,7 @@ type ForwardRes struct { } // GetObjectKind returns a schema object. -func (f ForwardRes) GetObjectKind() schema.ObjectKind { +func (ForwardRes) GetObjectKind() schema.ObjectKind { return nil } diff --git a/internal/render/pv.go b/internal/render/pv.go index c52add5e..e4b613a3 100644 --- a/internal/render/pv.go +++ b/internal/render/pv.go @@ -24,7 +24,7 @@ type PersistentVolume struct { } // ColorerFunc colors a resource row. -func (p PersistentVolume) ColorerFunc() model1.ColorerFunc { +func (PersistentVolume) ColorerFunc() model1.ColorerFunc { return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { c := model1.DefaultColorer(ns, h, re) @@ -49,41 +49,37 @@ func (p PersistentVolume) ColorerFunc() model1.ColorerFunc { // Header returns a header row. func (p PersistentVolume) Header(_ string) model1.Header { - return p.doHeader(p.defaultHeader()) + return p.doHeader(defaultPVHeader) } -func (PersistentVolume) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "CAPACITY", Attrs: model1.Attrs{Capacity: true}}, - model1.HeaderColumn{Name: "ACCESS MODES"}, - model1.HeaderColumn{Name: "RECLAIM POLICY"}, - model1.HeaderColumn{Name: "STATUS"}, - model1.HeaderColumn{Name: "CLAIM"}, - model1.HeaderColumn{Name: "STORAGECLASS"}, - model1.HeaderColumn{Name: "REASON"}, - model1.HeaderColumn{Name: "VOLUMEMODE", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } +var defaultPVHeader = model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "CAPACITY", Attrs: model1.Attrs{Capacity: true}}, + model1.HeaderColumn{Name: "ACCESS MODES"}, + model1.HeaderColumn{Name: "RECLAIM POLICY"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "CLAIM"}, + model1.HeaderColumn{Name: "STORAGECLASS"}, + model1.HeaderColumn{Name: "REASON"}, + model1.HeaderColumn{Name: "VOLUMEMODE", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, } // Render renders a K8s resource to screen. -func (p PersistentVolume) Render(o interface{}, ns string, row *model1.Row) error { +func (p PersistentVolume) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected PersistentVolume, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } - if err := p.defaultRow(raw, row); err != nil { return err } if p.specs.isEmpty() { return nil } - - cols, err := p.specs.realize(raw, p.defaultHeader(), row) + cols, err := p.specs.realize(raw, defaultPVHeader, row) if err != nil { return err } @@ -114,7 +110,7 @@ func (p PersistentVolume) defaultRow(raw *unstructured.Unstructured, r *model1.R size := pv.Spec.Capacity[v1.ResourceStorage] - r.ID = client.MetaFQN(pv.ObjectMeta) + r.ID = client.MetaFQN(&pv.ObjectMeta) r.Fields = model1.Fields{ pv.Name, size.String(), diff --git a/internal/render/pv_test.go b/internal/render/pv_test.go index 615fd8b6..2a8bac49 100644 --- a/internal/render/pv_test.go +++ b/internal/render/pv_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPersistentVolumeRender(t *testing.T) { c := render.PersistentVolume{} r := model1.NewRow(9) - assert.NoError(t, c.Render(load(t, "pv"), "-", &r)) + require.NoError(t, c.Render(load(t, "pv"), "-", &r)) assert.Equal(t, "-/pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", r.ID) assert.Equal(t, model1.Fields{"pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", "1Gi", "RWO", "Delete", "Bound", "default/www-nginx-sts-1", "standard"}, r.Fields[:7]) } @@ -24,7 +25,7 @@ func TestTerminatingPersistentVolumeRender(t *testing.T) { c := render.PersistentVolume{} r := model1.NewRow(9) - assert.NoError(t, c.Render(load(t, "pv_terminating"), "-", &r)) + require.NoError(t, c.Render(load(t, "pv_terminating"), "-", &r)) assert.Equal(t, "-/pvc-a4d86f51-916c-476b-83af-b551c91a8ac0", r.ID) assert.Equal(t, model1.Fields{"pvc-a4d86f51-916c-476b-83af-b551c91a8ac0", "1Gi", "RWO", "Delete", "Terminating", "default/www-nginx-sts-2", "standard"}, r.Fields[:7]) } diff --git a/internal/render/pvc.go b/internal/render/pvc.go index bb561c94..1e8181c3 100644 --- a/internal/render/pvc.go +++ b/internal/render/pvc.go @@ -13,6 +13,19 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +var defaultPVCHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "VOLUME"}, + model1.HeaderColumn{Name: "CAPACITY", Attrs: model1.Attrs{Capacity: true}}, + model1.HeaderColumn{Name: "ACCESS MODES"}, + model1.HeaderColumn{Name: "STORAGECLASS"}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // PersistentVolumeClaim renders a K8s PersistentVolumeClaim to screen. type PersistentVolumeClaim struct { Base @@ -20,29 +33,14 @@ type PersistentVolumeClaim struct { // Header returns a header row. func (p PersistentVolumeClaim) Header(_ string) model1.Header { - return p.doHeader(p.defaultHeader()) -} - -func (PersistentVolumeClaim) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "STATUS"}, - model1.HeaderColumn{Name: "VOLUME"}, - model1.HeaderColumn{Name: "CAPACITY", Attrs: model1.Attrs{Capacity: true}}, - model1.HeaderColumn{Name: "ACCESS MODES"}, - model1.HeaderColumn{Name: "STORAGECLASS"}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return p.doHeader(defaultPVCHeader) } // Render renders a K8s resource to screen. -func (p PersistentVolumeClaim) Render(o interface{}, ns string, row *model1.Row) error { +func (p PersistentVolumeClaim) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected PersistentVolumeClaim, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } if err := p.defaultRow(raw, row); err != nil { @@ -52,7 +50,7 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, row *model1.Row) return nil } - cols, err := p.specs.realize(raw, p.defaultHeader(), row) + cols, err := p.specs.realize(raw, defaultPVCHeader, row) if err != nil { return err } @@ -87,7 +85,7 @@ func (p PersistentVolumeClaim) defaultRow(raw *unstructured.Unstructured, r *mod } } - r.ID = client.MetaFQN(pvc.ObjectMeta) + r.ID = client.MetaFQN(&pvc.ObjectMeta) r.Fields = model1.Fields{ pvc.Namespace, pvc.Name, diff --git a/internal/render/pvc_test.go b/internal/render/pvc_test.go index ec85c2e1..3b361f3d 100644 --- a/internal/render/pvc_test.go +++ b/internal/render/pvc_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPersistentVolumeClaimRender(t *testing.T) { c := render.PersistentVolumeClaim{} r := model1.NewRow(8) - assert.NoError(t, c.Render(load(t, "pvc"), "", &r)) + require.NoError(t, c.Render(load(t, "pvc"), "", &r)) assert.Equal(t, "default/www-nginx-sts-0", r.ID) assert.Equal(t, model1.Fields{"default", "www-nginx-sts-0", "Bound", "pvc-fbabd470-8725-11e9-a8e8-42010a80015b", "1Gi", "RWO", "standard"}, r.Fields[:7]) } diff --git a/internal/render/rbac.go b/internal/render/rbac.go index 8869ad63..b5748c12 100644 --- a/internal/render/rbac.go +++ b/internal/render/rbac.go @@ -43,7 +43,7 @@ func (Rbac) ColorerFunc() model1.ColorerFunc { } // Header returns a header row. -func (Rbac) Header(ns string) model1.Header { +func (Rbac) Header(string) model1.Header { h := make(model1.Header, 0, 10) h = append(h, model1.HeaderColumn{Name: "NAME"}, @@ -55,10 +55,10 @@ func (Rbac) Header(ns string) model1.Header { } // Render renders a K8s resource to screen. -func (r Rbac) Render(o interface{}, ns string, ro *model1.Row) error { - p, ok := o.(PolicyRes) +func (r Rbac) Render(o any, ns string, ro *model1.Row) error { + p, ok := o.(*PolicyRes) if !ok { - return fmt.Errorf("expecting RuleRes but got %T", o) + return fmt.Errorf("expecting PolicyRes but got %T", o) } ro.ID = p.Resource @@ -135,8 +135,8 @@ type RuleRes struct { } // NewRuleRes returns a new rule. -func NewRuleRes(res, grp string, vv []string) RuleRes { - return RuleRes{ +func NewRuleRes(res, grp string, vv []string) *RuleRes { + return &RuleRes{ Resource: res, Group: grp, Verbs: vv, @@ -144,20 +144,20 @@ func NewRuleRes(res, grp string, vv []string) RuleRes { } // GetObjectKind returns a schema object. -func (r RuleRes) GetObjectKind() schema.ObjectKind { +func (*RuleRes) GetObjectKind() schema.ObjectKind { return nil } // DeepCopyObject returns a container copy. -func (r RuleRes) DeepCopyObject() runtime.Object { +func (r *RuleRes) DeepCopyObject() runtime.Object { return r } // Rules represents a collection of rules. -type Rules []RuleRes +type Rules []*RuleRes // Upsert adds a new rule. -func (rr Rules) Upsert(r RuleRes) Rules { +func (rr Rules) Upsert(r *RuleRes) Rules { idx, ok := rr.find(r.Resource) if !ok { return append(rr, r) diff --git a/internal/render/reference.go b/internal/render/reference.go index 21dec9d7..6e64f497 100644 --- a/internal/render/reference.go +++ b/internal/render/reference.go @@ -20,13 +20,13 @@ type Reference struct { // ColorerFunc colors a resource row. func (Reference) ColorerFunc() model1.ColorerFunc { - return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { + return func(string, model1.Header, *model1.RowEvent) tcell.Color { return tcell.ColorCadetBlue } } // Header returns a header row. -func (Reference) Header(ns string) model1.Header { +func (Reference) Header(string) model1.Header { return model1.Header{ model1.HeaderColumn{Name: "NAMESPACE"}, model1.HeaderColumn{Name: "NAME"}, @@ -36,7 +36,7 @@ func (Reference) Header(ns string) model1.Header { // Render renders a K8s resource to screen. // BOZO!! Pass in a row with pre-alloc fields?? -func (Reference) Render(o interface{}, ns string, r *model1.Row) error { +func (Reference) Render(o any, _ string, r *model1.Row) error { ref, ok := o.(ReferenceRes) if !ok { return fmt.Errorf("expected ReferenceRes, but got %T", o) diff --git a/internal/render/reference_test.go b/internal/render/reference_test.go index 46aaf700..9846b36e 100644 --- a/internal/render/reference_test.go +++ b/internal/render/reference_test.go @@ -6,27 +6,29 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestReferenceRender(t *testing.T) { o := render.ReferenceRes{ Namespace: "ns1", Name: "blee", - GVR: "v1/secrets", + GVR: client.SecGVR.String(), } var ( ref = render.Reference{} r model1.Row ) - assert.Nil(t, ref.Render(o, "fred", &r)) + require.NoError(t, ref.Render(o, "fred", &r)) assert.Equal(t, "ns1/blee", r.ID) assert.Equal(t, model1.Fields{ "ns1", "blee", - "v1/secrets", + client.SecGVR.String(), }, r.Fields) } diff --git a/internal/render/render_test.go b/internal/render/render_test.go index 13192cb9..c34f1b7c 100644 --- a/internal/render/render_test.go +++ b/internal/render/render_test.go @@ -9,7 +9,7 @@ import ( "os" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -17,11 +17,11 @@ import ( func load(t testing.TB, 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 } diff --git a/internal/render/ro.go b/internal/render/ro.go index 7bf03349..e1b7180f 100644 --- a/internal/render/ro.go +++ b/internal/render/ro.go @@ -20,34 +20,30 @@ type Role struct { // Header returns a header row. func (r Role) Header(_ string) model1.Header { - return r.doHeader(r.defaultHeader()) + return r.doHeader(defaultROHeader) } -func (Role) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } +var defaultROHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, } // Render renders a K8s resource to screen. -func (r Role) Render(o interface{}, _ string, row *model1.Row) error { +func (r Role) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected Role, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } - if err := r.defaultRow(raw, row); err != nil { return err } if r.specs.isEmpty() { return nil } - - cols, err := r.specs.realize(raw, r.defaultHeader(), row) + cols, err := r.specs.realize(raw, defaultROHeader, row) if err != nil { return err } @@ -56,14 +52,14 @@ func (r Role) Render(o interface{}, _ string, row *model1.Row) error { return nil } -func (r Role) defaultRow(raw *unstructured.Unstructured, row *model1.Row) error { +func (Role) defaultRow(raw *unstructured.Unstructured, row *model1.Row) error { var ro rbacv1.Role err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ro) if err != nil { return err } - row.ID = client.MetaFQN(ro.ObjectMeta) + row.ID = client.MetaFQN(&ro.ObjectMeta) row.Fields = model1.Fields{ ro.Namespace, ro.Name, diff --git a/internal/render/ro_test.go b/internal/render/ro_test.go index 5beb907d..2985c816 100644 --- a/internal/render/ro_test.go +++ b/internal/render/ro_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRoleRender(t *testing.T) { c := render.Role{} r := model1.NewRow(3) - assert.NoError(t, c.Render(load(t, "ro"), "", &r)) + require.NoError(t, c.Render(load(t, "ro"), "", &r)) assert.Equal(t, "default/blee", r.ID) assert.Equal(t, model1.Fields{"default", "blee"}, r.Fields[:2]) } diff --git a/internal/render/rob.go b/internal/render/rob.go index 0f025541..7724e749 100644 --- a/internal/render/rob.go +++ b/internal/render/rob.go @@ -14,6 +14,17 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +var defaultROBHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "ROLE"}, + model1.HeaderColumn{Name: "KIND"}, + model1.HeaderColumn{Name: "SUBJECTS"}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // RoleBinding renders a K8s RoleBinding to screen. type RoleBinding struct { Base @@ -21,37 +32,22 @@ type RoleBinding struct { // Header returns a header row. func (r RoleBinding) Header(_ string) model1.Header { - return r.doHeader(r.defaultHeader()) -} - -func (RoleBinding) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "ROLE"}, - model1.HeaderColumn{Name: "KIND"}, - model1.HeaderColumn{Name: "SUBJECTS"}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return r.doHeader(defaultROBHeader) } // Render renders a K8s resource to screen. -func (r RoleBinding) Render(o interface{}, ns string, row *model1.Row) error { +func (r RoleBinding) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected RoleBinding, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } - if err := r.defaultRow(raw, row); err != nil { return err } if r.specs.isEmpty() { return nil } - - cols, err := r.specs.realize(raw, r.defaultHeader(), row) + cols, err := r.specs.realize(raw, defaultROBHeader, row) if err != nil { return err } @@ -60,7 +56,7 @@ func (r RoleBinding) Render(o interface{}, ns string, row *model1.Row) error { return nil } -func (r RoleBinding) defaultRow(raw *unstructured.Unstructured, row *model1.Row) error { +func (RoleBinding) defaultRow(raw *unstructured.Unstructured, row *model1.Row) error { var rb rbacv1.RoleBinding err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rb) if err != nil { @@ -69,7 +65,7 @@ func (r RoleBinding) defaultRow(raw *unstructured.Unstructured, row *model1.Row) kind, ss := renderSubjects(rb.Subjects) - row.ID = client.MetaFQN(rb.ObjectMeta) + row.ID = client.MetaFQN(&rb.ObjectMeta) row.Fields = model1.Fields{ rb.Namespace, rb.Name, @@ -87,7 +83,7 @@ func (r RoleBinding) defaultRow(raw *unstructured.Unstructured, row *model1.Row) // ---------------------------------------------------------------------------- // Helpers... -func renderSubjects(ss []rbacv1.Subject) (kind string, subjects string) { +func renderSubjects(ss []rbacv1.Subject) (kind, subjects string) { if len(ss) == 0 { return NAValue, "" } @@ -101,7 +97,7 @@ func renderSubjects(ss []rbacv1.Subject) (kind string, subjects string) { } func toSubjectAlias(s string) string { - if len(s) == 0 { + if s == "" { return s } diff --git a/internal/render/rob_test.go b/internal/render/rob_test.go index f18a08bf..4f16fc73 100644 --- a/internal/render/rob_test.go +++ b/internal/render/rob_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRoleBindingRender(t *testing.T) { c := render.RoleBinding{} r := model1.NewRow(6) - assert.NoError(t, c.Render(load(t, "rb"), "", &r)) + require.NoError(t, c.Render(load(t, "rb"), "", &r)) assert.Equal(t, "default/blee", r.ID) assert.Equal(t, model1.Fields{"default", "blee", "blee", "SvcAcct", "fernand"}, r.Fields[:5]) } diff --git a/internal/render/rs.go b/internal/render/rs.go index 45f1bd1b..f9c1fa54 100644 --- a/internal/render/rs.go +++ b/internal/render/rs.go @@ -22,46 +22,42 @@ type ReplicaSet struct { } // ColorerFunc colors a resource row. -func (r ReplicaSet) ColorerFunc() model1.ColorerFunc { +func (ReplicaSet) ColorerFunc() model1.ColorerFunc { return model1.DefaultColorer } // Header returns a header row. func (r ReplicaSet) Header(_ string) model1.Header { - return r.doHeader(r.defaultHeader()) + return r.doHeader(defaultRSHeader) } -func (ReplicaSet) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, - model1.HeaderColumn{Name: "DESIRED", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "CURRENT", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "READY", Attrs: model1.Attrs{Align: tview.AlignRight}}, - model1.HeaderColumn{Name: "CONTAINERS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "IMAGES", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } +var defaultRSHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, + model1.HeaderColumn{Name: "DESIRED", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "CURRENT", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "READY", Attrs: model1.Attrs{Align: tview.AlignRight}}, + model1.HeaderColumn{Name: "CONTAINERS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "IMAGES", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, } // Render renders a K8s resource to screen. -func (r ReplicaSet) Render(o interface{}, ns string, row *model1.Row) error { +func (r ReplicaSet) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected ReplicaSet, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } - if err := r.defaultRow(raw, row); err != nil { return err } if r.specs.isEmpty() { return nil } - - cols, err := r.specs.realize(raw, r.defaultHeader(), row) + cols, err := r.specs.realize(raw, defaultRSHeader, row) if err != nil { return err } @@ -81,29 +77,29 @@ func (r ReplicaSet) defaultRow(raw *unstructured.Unstructured, row *model1.Row) cc = rs.Spec.Template.Spec.Containers cos, imgs = make([]string, 0, len(cc)), make([]string, 0, len(cc)) ) - for _, co := range cc { - cos, imgs = append(cos, co.Name), append(imgs, co.Image) + for i := range cc { + cos, imgs = append(cos, cc[i].Name), append(imgs, cc[i].Image) } - row.ID = client.MetaFQN(rs.ObjectMeta) + row.ID = client.MetaFQN(&rs.ObjectMeta) row.Fields = model1.Fields{ rs.Namespace, rs.Name, - computeVulScore(rs.ObjectMeta, &rs.Spec.Template.Spec), + computeVulScore(rs.Namespace, rs.Labels, &rs.Spec.Template.Spec), strconv.Itoa(int(*rs.Spec.Replicas)), strconv.Itoa(int(rs.Status.Replicas)), strconv.Itoa(int(rs.Status.ReadyReplicas)), strings.Join(cos, ","), strings.Join(imgs, ","), mapToStr(rs.Labels), - AsStatus(r.diagnose(rs)), + AsStatus(r.diagnose(&rs)), ToAge(rs.GetCreationTimestamp()), } return nil } -func (ReplicaSet) diagnose(rs appsv1.ReplicaSet) error { +func (ReplicaSet) diagnose(rs *appsv1.ReplicaSet) error { if rs.Status.Replicas != rs.Status.ReadyReplicas { if rs.Status.Replicas == 0 { return fmt.Errorf("did not phase down correctly expecting 0 replicas but got %d", rs.Status.ReadyReplicas) diff --git a/internal/render/rs_test.go b/internal/render/rs_test.go index 7a84cf38..29fc0563 100644 --- a/internal/render/rs_test.go +++ b/internal/render/rs_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestReplicaSetRender(t *testing.T) { c := render.ReplicaSet{} r := model1.NewRow(4) - assert.NoError(t, c.Render(load(t, "rs"), "", &r)) + require.NoError(t, c.Render(load(t, "rs"), "", &r)) assert.Equal(t, "icx/icx-db-7d4b578979", r.ID) assert.Equal(t, model1.Fields{"icx", "icx-db-7d4b578979", "0", "1", "1", "1"}, r.Fields[:6]) } diff --git a/internal/render/sa.go b/internal/render/sa.go index c6c801f0..f49a2fbb 100644 --- a/internal/render/sa.go +++ b/internal/render/sa.go @@ -14,6 +14,15 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +var defaultSAHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "SECRET"}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // ServiceAccount renders a K8s ServiceAccount to screen. type ServiceAccount struct { Base @@ -21,35 +30,22 @@ type ServiceAccount struct { // Header returns a header row. func (s ServiceAccount) Header(_ string) model1.Header { - return s.doHeader(s.defaultHeader()) -} - -func (ServiceAccount) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "SECRET"}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return s.doHeader(defaultSAHeader) } // Render renders a K8s resource to screen. -func (s ServiceAccount) Render(o interface{}, ns string, row *model1.Row) error { +func (s ServiceAccount) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected ServiceAccount, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } - if err := s.defaultRow(raw, row); err != nil { return err } if s.specs.isEmpty() { return nil } - - cols, err := s.specs.realize(raw, s.defaultHeader(), row) + cols, err := s.specs.realize(raw, defaultSAHeader, row) if err != nil { return err } @@ -58,14 +54,14 @@ func (s ServiceAccount) Render(o interface{}, ns string, row *model1.Row) error return nil } -func (s ServiceAccount) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error { +func (ServiceAccount) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error { var sa v1.ServiceAccount err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sa) if err != nil { return err } - r.ID = client.MetaFQN(sa.ObjectMeta) + r.ID = client.MetaFQN(&sa.ObjectMeta) r.Fields = model1.Fields{ sa.Namespace, sa.Name, diff --git a/internal/render/sa_test.go b/internal/render/sa_test.go index 932ee798..8615d114 100644 --- a/internal/render/sa_test.go +++ b/internal/render/sa_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestServiceAccountRender(t *testing.T) { c := render.ServiceAccount{} r := model1.NewRow(4) - assert.NoError(t, c.Render(load(t, "sa"), "", &r)) + require.NoError(t, c.Render(load(t, "sa"), "", &r)) assert.Equal(t, "default/blee", r.ID) assert.Equal(t, model1.Fields{"default", "blee", "2"}, r.Fields[:3]) } diff --git a/internal/render/sc.go b/internal/render/sc.go index 07a25592..cfbf27b8 100644 --- a/internal/render/sc.go +++ b/internal/render/sc.go @@ -15,6 +15,17 @@ import ( "k8s.io/kubectl/pkg/util/storage" ) +var defaultSCHeader = model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "PROVISIONER"}, + model1.HeaderColumn{Name: "RECLAIMPOLICY"}, + model1.HeaderColumn{Name: "VOLUMEBINDINGMODE"}, + model1.HeaderColumn{Name: "ALLOWVOLUMEEXPANSION"}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // StorageClass renders a K8s StorageClass to screen. type StorageClass struct { Base @@ -22,37 +33,22 @@ type StorageClass struct { // Header returns a header row. func (s StorageClass) Header(_ string) model1.Header { - return s.doHeader(s.defaultHeader()) -} - -func (StorageClass) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "PROVISIONER"}, - model1.HeaderColumn{Name: "RECLAIMPOLICY"}, - model1.HeaderColumn{Name: "VOLUMEBINDINGMODE"}, - model1.HeaderColumn{Name: "ALLOWVOLUMEEXPANSION"}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return s.doHeader(defaultSCHeader) } // Render renders a K8s resource to screen. -func (s StorageClass) Render(o interface{}, ns string, row *model1.Row) error { +func (s StorageClass) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected StorageClass, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } - if err := s.defaultRow(raw, row); err != nil { return err } if s.specs.isEmpty() { return nil } - - cols, err := s.specs.realize(raw, s.defaultHeader(), row) + cols, err := s.specs.realize(raw, defaultSCHeader, row) if err != nil { return err } @@ -70,7 +66,7 @@ func (s StorageClass) defaultRow(raw *unstructured.Unstructured, r *model1.Row) r.ID = client.FQN(client.ClusterScope, sc.Name) r.Fields = model1.Fields{ - s.nameWithDefault(sc.ObjectMeta), + s.nameWithDefault(&sc.ObjectMeta), sc.Provisioner, strPtrToStr((*string)(sc.ReclaimPolicy)), strPtrToStr((*string)(sc.VolumeBindingMode)), @@ -83,8 +79,8 @@ func (s StorageClass) defaultRow(raw *unstructured.Unstructured, r *model1.Row) return nil } -func (StorageClass) nameWithDefault(meta metav1.ObjectMeta) string { - if storage.IsDefaultAnnotationText(meta) == "Yes" { +func (StorageClass) nameWithDefault(meta *metav1.ObjectMeta) string { + if storage.IsDefaultAnnotationText(*meta) == "Yes" { return meta.Name + " (default)" } return meta.Name diff --git a/internal/render/sc_test.go b/internal/render/sc_test.go index c5883136..a0a340f3 100644 --- a/internal/render/sc_test.go +++ b/internal/render/sc_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestStorageClassRender(t *testing.T) { c := render.StorageClass{} r := model1.NewRow(4) - assert.NoError(t, c.Render(load(t, "sc"), "", &r)) + require.NoError(t, c.Render(load(t, "sc"), "", &r)) assert.Equal(t, "-/standard", r.ID) assert.Equal(t, model1.Fields{"standard (default)", "kubernetes.io/gce-pd", "Delete", "Immediate", "true"}, r.Fields[:5]) } diff --git a/internal/render/screen_dump.go b/internal/render/screen_dump.go index 6558616e..9d22f838 100644 --- a/internal/render/screen_dump.go +++ b/internal/render/screen_dump.go @@ -23,13 +23,13 @@ type ScreenDump struct { // ColorerFunc colors a resource row. func (ScreenDump) ColorerFunc() model1.ColorerFunc { - return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { + return func(string, model1.Header, *model1.RowEvent) tcell.Color { return tcell.ColorNavajoWhite } } // Header returns a header row. -func (ScreenDump) Header(ns string) model1.Header { +func (ScreenDump) Header(string) model1.Header { return model1.Header{ model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "DIR"}, @@ -39,7 +39,7 @@ func (ScreenDump) Header(ns string) model1.Header { } // Render renders a K8s resource to screen. -func (b ScreenDump) Render(o interface{}, ns string, r *model1.Row) error { +func (ScreenDump) Render(o any, _ string, r *model1.Row) error { f, ok := o.(FileRes) if !ok { return fmt.Errorf("expecting screendumper, but got %T", o) @@ -70,7 +70,7 @@ type FileRes struct { } // GetObjectKind returns a schema object. -func (c FileRes) GetObjectKind() schema.ObjectKind { +func (FileRes) GetObjectKind() schema.ObjectKind { return nil } diff --git a/internal/render/screen_dump_test.go b/internal/render/screen_dump_test.go index 4831587c..a18acc25 100644 --- a/internal/render/screen_dump_test.go +++ b/internal/render/screen_dump_test.go @@ -11,6 +11,7 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestScreenDumpRender(t *testing.T) { @@ -21,7 +22,7 @@ func TestScreenDumpRender(t *testing.T) { Dir: "fred/blee", } - assert.Nil(t, s.Render(o, "fred", &r)) + require.NoError(t, s.Render(o, "fred", &r)) assert.Equal(t, "fred/blee/bob", r.ID) assert.Equal(t, model1.Fields{ "bob", @@ -36,12 +37,12 @@ type fileInfo struct{} var _ os.FileInfo = fileInfo{} -func (f fileInfo) Name() string { return "bob" } -func (f fileInfo) Size() int64 { return 100 } -func (f fileInfo) ModTime() time.Time { return testTime() } -func (f fileInfo) IsDir() bool { return false } -func (f fileInfo) Sys() interface{} { return nil } +func (fileInfo) Name() string { return "bob" } +func (fileInfo) Size() int64 { return 100 } +func (fileInfo) ModTime() time.Time { return testTime() } +func (fileInfo) IsDir() bool { return false } +func (fileInfo) Sys() any { return nil } -func (f fileInfo) Mode() os.FileMode { +func (fileInfo) Mode() os.FileMode { return os.FileMode(0644) } diff --git a/internal/render/secret.go b/internal/render/secret.go index ac78e5a6..7ba6e6c4 100644 --- a/internal/render/secret.go +++ b/internal/render/secret.go @@ -14,6 +14,15 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +var defaultSECHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "TYPE"}, + model1.HeaderColumn{Name: "DATA"}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // Secret renders a K8s Secret to screen. type Secret struct { Base @@ -21,35 +30,22 @@ type Secret struct { // Header returns a header row. func (s Secret) Header(_ string) model1.Header { - return s.doHeader(s.defaultHeader()) -} - -func (Secret) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "TYPE"}, - model1.HeaderColumn{Name: "DATA"}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return s.doHeader(defaultSECHeader) } // Render renders a K8s resource to screen. -func (s Secret) Render(o interface{}, ns string, row *model1.Row) error { +func (s Secret) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected Secret, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } - if err := s.defaultRow(raw, row); err != nil { return err } if s.specs.isEmpty() { return nil } - - cols, err := s.specs.realize(raw, s.defaultHeader(), row) + cols, err := s.specs.realize(raw, defaultSECHeader, row) if err != nil { return err } @@ -58,7 +54,7 @@ func (s Secret) Render(o interface{}, ns string, row *model1.Row) error { return nil } -func (n Secret) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error { +func (Secret) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error { var sec v1.Secret err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sec) if err != nil { diff --git a/internal/render/sts.go b/internal/render/sts.go index 45dd6bc6..4293ada2 100644 --- a/internal/render/sts.go +++ b/internal/render/sts.go @@ -14,6 +14,20 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +var defaultSTSHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "SERVICE"}, + model1.HeaderColumn{Name: "CONTAINERS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "IMAGES", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // StatefulSet renders a K8s StatefulSet to screen. type StatefulSet struct { Base @@ -21,40 +35,22 @@ type StatefulSet struct { // Header returns a header row. func (s StatefulSet) Header(_ string) model1.Header { - return s.doHeader(s.defaultHeader()) -} - -func (StatefulSet) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "VS", Attrs: model1.Attrs{VS: true}}, - model1.HeaderColumn{Name: "READY"}, - model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "SERVICE"}, - model1.HeaderColumn{Name: "CONTAINERS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "IMAGES", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return s.doHeader(defaultSTSHeader) } // Render renders a K8s resource to screen. -func (s StatefulSet) Render(o interface{}, ns string, row *model1.Row) error { +func (s StatefulSet) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected StatefulSet, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } - if err := s.defaultRow(raw, row); err != nil { return err } if s.specs.isEmpty() { return nil } - - cols, err := s.specs.realize(raw, s.defaultHeader(), row) + cols, err := s.specs.realize(raw, defaultSTSHeader, row) if err != nil { return err } @@ -70,16 +66,16 @@ func (s StatefulSet) defaultRow(raw *unstructured.Unstructured, r *model1.Row) e return err } - r.ID = client.MetaFQN(sts.ObjectMeta) + r.ID = client.MetaFQN(&sts.ObjectMeta) r.Fields = model1.Fields{ sts.Namespace, sts.Name, - computeVulScore(sts.ObjectMeta, &sts.Spec.Template.Spec), + computeVulScore(sts.Namespace, sts.Labels, &sts.Spec.Template.Spec), strconv.Itoa(int(sts.Status.ReadyReplicas)) + "/" + strconv.Itoa(int(sts.Status.Replicas)), asSelector(sts.Spec.Selector), na(sts.Spec.ServiceName), - podContainerNames(sts.Spec.Template.Spec, true), - podImageNames(sts.Spec.Template.Spec, true), + podContainerNames(&sts.Spec.Template.Spec, true), + podImageNames(&sts.Spec.Template.Spec, true), mapToStr(sts.Labels), AsStatus(s.diagnose(sts.Spec.Replicas, sts.Status.Replicas, sts.Status.ReadyReplicas)), ToAge(sts.GetCreationTimestamp()), diff --git a/internal/render/sts_test.go b/internal/render/sts_test.go index d8a4edc8..0caaf850 100644 --- a/internal/render/sts_test.go +++ b/internal/render/sts_test.go @@ -9,13 +9,14 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestStatefulSetRender(t *testing.T) { c := render.StatefulSet{} r := model1.NewRow(4) - assert.Nil(t, c.Render(load(t, "sts"), "", &r)) + require.NoError(t, c.Render(load(t, "sts"), "", &r)) assert.Equal(t, "default/nginx-sts", r.ID) assert.Equal(t, model1.Fields{"default", "nginx-sts", "0", "4/4", "app=nginx-sts", "nginx-sts", "nginx", "k8s.gcr.io/nginx-slim:0.8", "app=nginx-sts", ""}, r.Fields[:len(r.Fields)-1]) } diff --git a/internal/render/subject.go b/internal/render/subject.go index bb0f1b9a..6d254dc3 100644 --- a/internal/render/subject.go +++ b/internal/render/subject.go @@ -19,13 +19,13 @@ type Subject struct { // ColorerFunc colors a resource row. func (Subject) ColorerFunc() model1.ColorerFunc { - return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { + return func(string, model1.Header, *model1.RowEvent) tcell.Color { return tcell.ColorMediumSpringGreen } } // Header returns a header row. -func (Subject) Header(ns string) model1.Header { +func (Subject) Header(string) model1.Header { return model1.Header{ model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "KIND"}, @@ -35,7 +35,7 @@ func (Subject) Header(ns string) model1.Header { } // Render renders a K8s resource to screen. -func (s Subject) Render(o interface{}, ns string, r *model1.Row) error { +func (s Subject) Render(o any, _ string, r *model1.Row) error { res, ok := o.(SubjectRes) if !ok { return fmt.Errorf("expected SubjectRes, but got %T", s) diff --git a/internal/render/svc.go b/internal/render/svc.go index 43b7bb3a..cfa71f8d 100644 --- a/internal/render/svc.go +++ b/internal/render/svc.go @@ -16,6 +16,20 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// Header returns a header row. +var defaultSVCHeader = model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "TYPE"}, + model1.HeaderColumn{Name: "CLUSTER-IP"}, + model1.HeaderColumn{Name: "EXTERNAL-IP"}, + model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "PORTS", Attrs: model1.Attrs{Wide: false}}, + model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // Service renders a K8s Service to screen. type Service struct { Base @@ -23,40 +37,22 @@ type Service struct { // Header returns a header row. func (s Service) Header(_ string) model1.Header { - return s.doHeader(s.defaultHeader()) -} - -// Header returns a header row. -func (Service) defaultHeader() model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "TYPE"}, - model1.HeaderColumn{Name: "CLUSTER-IP"}, - model1.HeaderColumn{Name: "EXTERNAL-IP"}, - model1.HeaderColumn{Name: "SELECTOR", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "PORTS", Attrs: model1.Attrs{Wide: false}}, - model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return s.doHeader(defaultSVCHeader) } // Render renders a K8s resource to screen. -func (s Service) Render(o interface{}, ns string, row *model1.Row) error { +func (s Service) Render(o any, _ string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("expected Service, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } - if err := s.defaultRow(raw, row); err != nil { return err } if s.specs.isEmpty() { return nil } - - cols, err := s.specs.realize(raw, s.defaultHeader(), row) + cols, err := s.specs.realize(raw, defaultSVCHeader, row) if err != nil { return err } @@ -72,7 +68,7 @@ func (s Service) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error return err } - r.ID = client.MetaFQN(svc.ObjectMeta) + r.ID = client.MetaFQN(&svc.ObjectMeta) r.Fields = model1.Fields{ svc.Namespace, svc.Name, @@ -107,19 +103,17 @@ func getSvcExtIPS(svc *v1.Service) []string { results := []string{} switch svc.Spec.Type { - case v1.ServiceTypeClusterIP: - fallthrough - case v1.ServiceTypeNodePort: + case v1.ServiceTypeNodePort, v1.ServiceTypeClusterIP: return svc.Spec.ExternalIPs case v1.ServiceTypeLoadBalancer: lbIps := lbIngressIP(svc.Status.LoadBalancer) if len(svc.Spec.ExternalIPs) > 0 { - if len(lbIps) > 0 { + if lbIps != "" { results = append(results, lbIps) } return append(results, svc.Spec.ExternalIPs...) } - if len(lbIps) > 0 { + if lbIps != "" { results = append(results, lbIps) } case v1.ServiceTypeExternalName: @@ -133,9 +127,9 @@ func lbIngressIP(s v1.LoadBalancerStatus) string { ingress := s.Ingress result := []string{} for i := range ingress { - if len(ingress[i].IP) > 0 { + if ingress[i].IP != "" { result = append(result, ingress[i].IP) - } else if len(ingress[i].Hostname) > 0 { + } else if ingress[i].Hostname != "" { result = append(result, ingress[i].Hostname) } } @@ -159,7 +153,7 @@ func toIPs(svcType v1.ServiceType, ips []string) string { func ToPorts(pp []v1.ServicePort) string { ports := make([]string, len(pp)) for i, p := range pp { - if len(p.Name) > 0 { + if p.Name != "" { ports[i] = p.Name + ":" } ports[i] += strconv.Itoa(int(p.Port)) + diff --git a/internal/render/svc_test.go b/internal/render/svc_test.go index 6a1ea62f..fe67c674 100644 --- a/internal/render/svc_test.go +++ b/internal/render/svc_test.go @@ -9,24 +9,28 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestServiceRender(t *testing.T) { c := render.Service{} r := model1.NewRow(4) - assert.NoError(t, c.Render(load(t, "svc"), "", &r)) + require.NoError(t, c.Render(load(t, "svc"), "", &r)) assert.Equal(t, "default/dictionary1", r.ID) assert.Equal(t, model1.Fields{"default", "dictionary1", "ClusterIP", "10.47.248.116", "", "app=dictionary1", "http:4001โ–บ0"}, r.Fields[:7]) } func BenchmarkSvcRender(b *testing.B) { - var svc render.Service - r := model1.NewRow(4) - s := load(b, "svc") + var ( + svc render.Service + r = model1.NewRow(4) + s = load(b, "svc") + ) + b.ResetTimer() b.ReportAllocs() - for i := 0; i < b.N; i++ { + for range b.N { _ = svc.Render(s, "", &r) } } diff --git a/internal/render/table.go b/internal/render/table.go index e509e77c..ab9d451c 100644 --- a/internal/render/table.go +++ b/internal/render/table.go @@ -8,6 +8,7 @@ import ( "fmt" "log/slog" "strings" + "sync" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/model1" @@ -23,12 +24,25 @@ type Table struct { table *metav1.Table header model1.Header ageIndex int + mx sync.RWMutex } func (*Table) IsGeneric() bool { return true } +func (t *Table) setAgeIndex(idx int) { + t.mx.Lock() + defer t.mx.Unlock() + t.ageIndex = idx +} + +func (t *Table) getAgeIndex() int { + t.mx.RLock() + defer t.mx.RUnlock() + return t.ageIndex +} + // SetTable sets the tabular resource. func (t *Table) SetTable(ns string, table *metav1.Table) { t.table = table @@ -41,7 +55,7 @@ func (*Table) ColorerFunc() model1.ColorerFunc { } // Header returns a header row. -func (t *Table) Header(ns string) model1.Header { +func (t *Table) Header(string) model1.Header { return t.doHeader(t.defaultHeader()) } @@ -53,12 +67,12 @@ func (t *Table) defaultHeader() model1.Header { h := make(model1.Header, 0, len(t.table.ColumnDefinitions)) for i, c := range t.table.ColumnDefinitions { if c.Name == ageTableCol { - t.ageIndex = i + t.setAgeIndex(i) continue } h = append(h, model1.HeaderColumn{Name: strings.ToUpper(c.Name)}) } - if t.ageIndex > 0 { + if t.getAgeIndex() > 0 { h = append(h, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}) } @@ -69,16 +83,14 @@ func (t *Table) defaultHeader() model1.Header { func (t *Table) Render(o any, ns string, r *model1.Row) error { row, ok := o.(metav1.TableRow) if !ok { - return fmt.Errorf("expected Table, but got %T", o) + return fmt.Errorf("expected TableRow, but got %T", o) } - if err := t.defaultRow(&row, ns, r); err != nil { return err } if t.specs.isEmpty() { return nil } - cols, err := t.specs.realize(row.Object.Object, t.defaultHeader(), r) if err != nil { return err @@ -89,7 +101,7 @@ func (t *Table) Render(o any, ns string, r *model1.Row) error { } func (t *Table) defaultRow(row *metav1.TableRow, ns string, r *model1.Row) error { - th := t.defaultHeader() + th := t.header ons, name := ns, UnknownValue switch { case row.Object.Object != nil: @@ -103,11 +115,11 @@ func (t *Table) defaultRow(row *metav1.TableRow, ns string, r *model1.Row) error } ons, name = pm.Namespace, pm.Name default: - if idx, ok := th.IndexOf("NAME", true); ok && idx >= 0 { + if idx, ok := th.IndexOf("NAME", true); ok && idx >= 0 && idx < len(row.Cells) { name = row.Cells[idx].(string) - if idx, ok := th.IndexOf("NAMESPACE", true); ok && idx >= 0 { - ons = row.Cells[idx].(string) - } + } + if idx, ok := th.IndexOf("NAMESPACE", true); ok && idx >= 0 && idx < len(row.Cells) { + ons = row.Cells[idx].(string) } } @@ -116,9 +128,12 @@ func (t *Table) defaultRow(row *metav1.TableRow, ns string, r *model1.Row) error } r.ID = client.FQN(ons, name) r.Fields = make(model1.Fields, 0, len(th)) - var age any + var ( + age any + ageIdx = t.getAgeIndex() + ) for i, c := range row.Cells { - if t.ageIndex > 0 && i == t.ageIndex { + if ageIdx > 0 && i == ageIdx { age = c continue } @@ -130,7 +145,7 @@ func (t *Table) defaultRow(row *metav1.TableRow, ns string, r *model1.Row) error } if d, ok := age.(string); ok { r.Fields = append(r.Fields, d) - } else if t.ageIndex > 0 { + } else if ageIdx > 0 { slog.Warn("No Duration detected on age field") r.Fields = append(r.Fields, NAValue) } diff --git a/internal/render/table_test.go b/internal/render/table_test.go index add7b484..2ca5933d 100644 --- a/internal/render/table_test.go +++ b/internal/render/table_test.go @@ -11,6 +11,7 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/runtime" @@ -83,7 +84,7 @@ func TestGenericRender(t *testing.T) { re.SetTable(u.ns, u.table) assert.Equal(t, u.eHeader, re.Header(u.ns)) - assert.Nil(t, re.Render(u.table.Rows[0], u.ns, &r)) + require.NoError(t, re.Render(u.table.Rows[0], u.ns, &r)) assert.Equal(t, u.eID, r.ID) assert.Equal(t, u.eFields, r.Fields) }) @@ -130,7 +131,7 @@ func TestGenericCustRender(t *testing.T) { re.SetTable(u.ns, u.table) assert.Equal(t, u.eHeader, re.Header(u.ns)) - assert.Nil(t, re.Render(u.table.Rows[0], u.ns, &r)) + require.NoError(t, re.Render(u.table.Rows[0], u.ns, &r)) assert.Equal(t, u.eID, r.ID) assert.Equal(t, u.eFields, r.Fields) }) @@ -152,17 +153,17 @@ func makeNSGeneric() *metav1beta1.Table { { Object: runtime.RawExtension{ Object: &unstructured.Unstructured{ - Object: map[string]interface{}{ + Object: map[string]any{ "kind": "fred", "apiVersion": "v1", - "metadata": map[string]interface{}{ + "metadata": map[string]any{ "namespace": "ns1", "name": "fred", }, }, }, }, - Cells: []interface{}{ + Cells: []any{ "ns1", "c1", "c2", @@ -184,16 +185,16 @@ func makeNoNSGeneric() *metav1beta1.Table { { Object: runtime.RawExtension{ Object: &unstructured.Unstructured{ - Object: map[string]interface{}{ + Object: map[string]any{ "kind": "fred", "apiVersion": "v1", - "metadata": map[string]interface{}{ + "metadata": map[string]any{ "name": "fred", }, }, }, }, - Cells: []interface{}{ + Cells: []any{ "c1", "c2", "c3", @@ -214,16 +215,16 @@ func makeAgeGeneric() *metav1beta1.Table { { Object: runtime.RawExtension{ Object: &unstructured.Unstructured{ - Object: map[string]interface{}{ + Object: map[string]any{ "kind": "fred", "apiVersion": "v1", - "metadata": map[string]interface{}{ + "metadata": map[string]any{ "name": "fred", }, }, }, }, - Cells: []interface{}{ + Cells: []any{ "c1", "2d", "c2", diff --git a/internal/render/workload.go b/internal/render/workload.go index b06e7300..dbfcfeaf 100644 --- a/internal/render/workload.go +++ b/internal/render/workload.go @@ -14,13 +14,23 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) +var defaultWKHeader = model1.Header{ + model1.HeaderColumn{Name: "KIND"}, + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, + model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, +} + // Workload renders a workload to screen. type Workload struct { Base } // ColorerFunc colors a resource row. -func (n Workload) ColorerFunc() model1.ColorerFunc { +func (Workload) ColorerFunc() model1.ColorerFunc { return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { c := model1.DefaultColorer(ns, h, re) @@ -39,22 +49,14 @@ func (n Workload) ColorerFunc() model1.ColorerFunc { // Header returns a header rbw. func (Workload) Header(string) model1.Header { - return model1.Header{ - model1.HeaderColumn{Name: "KIND"}, - model1.HeaderColumn{Name: "NAMESPACE"}, - model1.HeaderColumn{Name: "NAME"}, - model1.HeaderColumn{Name: "STATUS"}, - model1.HeaderColumn{Name: "READY"}, - model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}}, - model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}}, - } + return defaultWKHeader } // Render renders a K8s resource to screen. -func (n Workload) Render(o interface{}, _ string, r *model1.Row) error { +func (Workload) Render(o any, _ string, r *model1.Row) error { res, ok := o.(*WorkloadRes) if !ok { - return fmt.Errorf("expected allRes but got %T", o) + return fmt.Errorf("expected WorkloadRes but got %T", o) } r.ID = fmt.Sprintf("%s|%s|%s", res.Row.Cells[0].(string), res.Row.Cells[1].(string), res.Row.Cells[2].(string)) @@ -76,7 +78,7 @@ type WorkloadRes struct { } // GetObjectKind returns a schema object. -func (a *WorkloadRes) GetObjectKind() schema.ObjectKind { +func (*WorkloadRes) GetObjectKind() schema.ObjectKind { return nil } diff --git a/internal/slogs/keys.go b/internal/slogs/keys.go index 7ece5324..24ee165a 100644 --- a/internal/slogs/keys.go +++ b/internal/slogs/keys.go @@ -212,4 +212,10 @@ const ( // ID tracks an id logger key. ID = "id" + + // ViewSetting tracks a view setting logger key. + ViewSetting = "view-setting" + + // JQExp tracks a jq expression logger key. + JQExp = "jq-exp" ) diff --git a/internal/tchart/component.go b/internal/tchart/component.go index b557631e..a0a17458 100644 --- a/internal/tchart/component.go +++ b/internal/tchart/component.go @@ -24,7 +24,8 @@ type Component struct { focusFgColor, focusBgColor string seriesColors []tcell.Color dimmed tcell.Style - id, legend string + id string + legend string blur func(tcell.Key) mx sync.RWMutex } @@ -67,7 +68,7 @@ func (c *Component) SetLegend(l string) { // InputHandler returns the handler for this primitive. func (c *Component) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { return c.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { - // nolint:exhaustive + //nolint:exhaustive switch key := event.Key(); key { case tcell.KeyEnter: case tcell.KeyBacktab, tcell.KeyTab: @@ -80,7 +81,7 @@ func (c *Component) InputHandler() func(event *tcell.EventKey, setFocus func(p t } // IsDial returns true if chart is a dial. -func (c *Component) IsDial() bool { +func (*Component) IsDial() bool { return false } @@ -117,13 +118,13 @@ func (c *Component) GetSeriesColorNames() []string { return nn } -func (c *Component) colorForSeries() (tcell.Color, tcell.Color) { +func (c *Component) colorForSeries() (cool, fault tcell.Color) { c.mx.RLock() defer c.mx.RUnlock() - if len(c.seriesColors) == 2 { return c.seriesColors[0], c.seriesColors[1] } + return okColor, faultColor } diff --git a/internal/tchart/dot_matrix.go b/internal/tchart/dot_matrix.go index ae6b1bcc..8c424982 100644 --- a/internal/tchart/dot_matrix.go +++ b/internal/tchart/dot_matrix.go @@ -45,7 +45,7 @@ func NewDotMatrix() DotMatrix { } // Print prints the matrix. -func (d DotMatrix) Print(n int) Matrix { +func (DotMatrix) Print(n int) Matrix { return To3x3Char(n) } diff --git a/internal/tchart/dot_matrix_test.go b/internal/tchart/dot_matrix_test.go index df1c85d2..d15a0e6d 100644 --- a/internal/tchart/dot_matrix_test.go +++ b/internal/tchart/dot_matrix_test.go @@ -13,7 +13,7 @@ import ( func TestDial3x3(t *testing.T) { d := tchart.NewDotMatrix() - for n := 0; n <= 2; n++ { + for n := range 2 { i := n t.Run(strconv.Itoa(n), func(t *testing.T) { assert.Equal(t, tchart.To3x3Char(i), d.Print(i)) diff --git a/internal/tchart/gauge.go b/internal/tchart/gauge.go index 5b830528..273eedbf 100644 --- a/internal/tchart/gauge.go +++ b/internal/tchart/gauge.go @@ -48,7 +48,7 @@ func (g *Gauge) SetResolution(n int) { } // IsDial returns true if chart is a dial. -func (g *Gauge) IsDial() bool { +func (*Gauge) IsDial() bool { return true } @@ -81,12 +81,12 @@ func (g *Gauge) Draw(sc tcell.Screen) { style = style.Foreground(tcell.ColorYellow) sc.SetContent(mid.X, mid.Y, 'โ ”', nil, style) - max := g.data.MaxDigits() - if max < g.resolution { - max = g.resolution + maxD := g.data.MaxDigits() + if maxD < g.resolution { + maxD = g.resolution } var ( - fmat = "%" + fmt.Sprintf(gaugeFmt, max) + fmat = "%" + fmt.Sprintf(gaugeFmt, maxD) o = image.Point{X: mid.X, Y: mid.Y - 1} ) @@ -118,7 +118,7 @@ func (g *Gauge) drawNum(sc tcell.Screen, o image.Point, n number, style tcell.St if significant { style = g.dimmed } - for i := 0; i < len(n.str); i++ { + for i := range len(n.str) { if n.str[i] == '0' && !significant { g.drawDial(sc, dm.Print(int(n.str[i]-48)), o, g.dimmed) } else { @@ -134,8 +134,8 @@ func (g *Gauge) drawNum(sc tcell.Screen, o image.Point, n number, style tcell.St } func (g *Gauge) drawDial(sc tcell.Screen, m Matrix, o image.Point, style tcell.Style) { - for r := 0; r < len(m); r++ { - for c := 0; c < len(m[r]); c++ { + for r := range m { + for c := range m[r] { dot := m[r][c] if dot == dots[0] { sc.SetContent(o.X+c, o.Y+r, dots[1], nil, g.dimmed) @@ -167,7 +167,7 @@ func computeDelta(d1, d2 int64) delta { func printDelta(sc tcell.Screen, d delta, o image.Point, s tcell.Style) { s = s.Dim(false) - // nolint:exhaustive + //nolint:exhaustive switch d { case DeltaLess: sc.SetContent(o.X-1, o.Y+1, 'โ†“', nil, s) diff --git a/internal/tchart/sparkline.go b/internal/tchart/sparkline.go index 2df126b0..6376dc99 100644 --- a/internal/tchart/sparkline.go +++ b/internal/tchart/sparkline.go @@ -91,7 +91,7 @@ func (s *SparkLine) Draw(screen tcell.Screen) { rect := s.asRect() s.cutSet(rect.Dx()) - max := s.computeMax() + maxVal := s.computeMax() cX, idx := rect.Min.X+1, 0 if len(s.data)*2 < rect.Dx() { @@ -100,7 +100,7 @@ func (s *SparkLine) Draw(screen tcell.Screen) { idx = len(s.data) - rect.Dx()/2 } - scale := float64(len(sparks)*(rect.Dy()-pad)) / float64(max) + scale := float64(len(sparks)*(rect.Dy()-pad)) / float64(maxVal) c1, c2 := s.colorForSeries() for _, d := range s.data[idx:] { b := toBlocks(d, scale) @@ -124,7 +124,7 @@ func (s *SparkLine) drawBlock(r image.Rectangle, screen tcell.Screen, x, y int, style := tcell.StyleDefault.Foreground(c).Background(s.bgColor) zeroY := r.Max.Y - r.Dy() - for i := 0; i < b.full; i++ { + for range b.full { screen.SetContent(x, y, sparks[len(sparks)-1], nil, style) y-- if y <= zeroY { @@ -147,15 +147,15 @@ func (s *SparkLine) cutSet(width int) { } func (s *SparkLine) computeMax() int64 { - var max int64 + var maxVal int64 for _, d := range s.data { m := d.Max() - if max < m { - max = m + if maxVal < m { + maxVal = m } } - return max + return maxVal } func toBlocks(m Metric, scale float64) blocks { diff --git a/internal/tchart/sparkline_int_test.go b/internal/tchart/sparkline_int_test.go index 14f0cd9b..b7345e1b 100644 --- a/internal/tchart/sparkline_int_test.go +++ b/internal/tchart/sparkline_int_test.go @@ -45,7 +45,7 @@ func TestCutSet(t *testing.T) { s.Add(m) } s.cutSet(u.w) - assert.Equal(t, u.e, len(s.data)) + assert.Len(t, s.data, u.e) }) } } diff --git a/internal/ui/action.go b/internal/ui/action.go index 305d3624..4a6f3591 100644 --- a/internal/ui/action.go +++ b/internal/ui/action.go @@ -5,7 +5,7 @@ package ui import ( "log/slog" - "sort" + "slices" "sync" "github.com/derailed/k9s/internal/model" @@ -110,9 +110,7 @@ func (a *KeyActions) Reset(aa *KeyActions) { func (a *KeyActions) Range(f RangeFn) { var km KeyMap a.mx.RLock() - { - km = a.actions - } + km = a.actions a.mx.RUnlock() for k, v := range km { @@ -195,22 +193,22 @@ func (a *KeyActions) Hints() model.MenuHints { a.mx.RLock() defer a.mx.RUnlock() - kk := make([]int, 0, len(a.actions)) + kk := make([]tcell.Key, 0, len(a.actions)) for k := range a.actions { if !a.actions[k].Opts.Shared { - kk = append(kk, int(k)) + kk = append(kk, k) } } - sort.Ints(kk) + slices.Sort(kk) hh := make(model.MenuHints, 0, len(kk)) for _, k := range kk { - if name, ok := tcell.KeyNames[tcell.Key(int16(k))]; ok { + if name, ok := tcell.KeyNames[k]; ok { hh = append(hh, model.MenuHint{ Mnemonic: name, - Description: a.actions[tcell.Key(k)].Description, - Visible: a.actions[tcell.Key(k)].Opts.Visible, + Description: a.actions[k].Description, + Visible: a.actions[k].Opts.Visible, }, ) } else { diff --git a/internal/ui/action_test.go b/internal/ui/action_test.go index 60733aa8..213dc8bf 100644 --- a/internal/ui/action_test.go +++ b/internal/ui/action_test.go @@ -20,6 +20,6 @@ func TestKeyActionsHints(t *testing.T) { hh := kk.Hints() - assert.Equal(t, 3, len(hh)) + assert.Len(t, hh, 3) assert.Equal(t, model.MenuHint{Mnemonic: "b", Description: "blee", Visible: true}, hh[0]) } diff --git a/internal/ui/app.go b/internal/ui/app.go index cbfb1a64..a1f21b7b 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -31,7 +31,7 @@ type App struct { } // NewApp returns a new app. -func NewApp(cfg *config.Config, context string) *App { +func NewApp(cfg *config.Config, _ string) *App { a := App{ Application: tview.NewApplication(), actions: NewKeyActions(), @@ -96,13 +96,13 @@ func (a *App) SetRunning(f bool) { } // BufferCompleted indicates input was accepted. -func (a *App) BufferCompleted(_, _ string) {} +func (*App) BufferCompleted(_, _ string) {} // BufferChanged indicates the buffer was changed. -func (a *App) BufferChanged(_, _ string) {} +func (*App) BufferChanged(_, _ string) {} // BufferActive indicates the buff activity changed. -func (a *App) BufferActive(state bool, kind model.BufferKind) { +func (a *App) BufferActive(state bool, _ model.BufferKind) { flex, ok := a.Main.GetPrimitive("main").(*tview.Flex) if !ok { return @@ -117,7 +117,7 @@ func (a *App) BufferActive(state bool, kind model.BufferKind) { } // SuggestionChanged notifies of update to command suggestions. -func (a *App) SuggestionChanged(ss []string) {} +func (*App) SuggestionChanged([]string) {} // StylesChanged notifies the skin changed. func (a *App) StylesChanged(s *config.Styles) { @@ -174,7 +174,7 @@ func (a *App) ResetCmd() { a.cmdBuff.Reset() } -func (a *App) saveCmd(evt *tcell.EventKey) *tcell.EventKey { +func (a *App) saveCmd(*tcell.EventKey) *tcell.EventKey { if err := a.Config.Save(true); err != nil { a.Flash().Err(err) } diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go index 2c4b1372..0725561a 100644 --- a/internal/ui/app_test.go +++ b/internal/ui/app_test.go @@ -36,7 +36,7 @@ func TestAppResetCmd(t *testing.T) { a.ResetCmd() - assert.Equal(t, "", a.CmdBuff().GetText()) + assert.Empty(t, a.CmdBuff().GetText()) } func TestAppHasCmd(t *testing.T) { diff --git a/internal/ui/config.go b/internal/ui/config.go index a4ab43df..0a4cdeaf 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -83,6 +83,7 @@ func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) e if err := w.Add(config.AppViewsFile); err != nil { return err } + slog.Debug("Loading custom views", slogs.FileName, config.AppViewsFile) return c.RefreshCustomViews() } @@ -182,7 +183,7 @@ func (c *Configurator) ConfigWatcher(ctx context.Context, s synchronizer) error if !ok { return nil } - ctConfigFile := filepath.Join(config.AppContextConfig(cl, ct)) + ctConfigFile := config.AppContextConfig(cl, ct) slog.Debug("ConfigWatcher watching", slogs.FileName, ctConfigFile) return w.Add(ctConfigFile) @@ -214,7 +215,7 @@ func (c *Configurator) activeSkin() (string, bool) { return skin, skin != "" } -func (c *Configurator) activeConfig() (cluster string, context string, ok bool) { +func (c *Configurator) activeConfig() (cluster, context string, ok bool) { if c.Config == nil || c.Config.K9s == nil { return } @@ -254,7 +255,7 @@ func (c *Configurator) RefreshStyles(s synchronizer) { } } -func (c *Configurator) loadSkinFile(s synchronizer) { +func (c *Configurator) loadSkinFile(synchronizer) { skin, ok := c.activeSkin() if !ok { slog.Debug("No custom skin found. Using stock skin") diff --git a/internal/ui/config_int_test.go b/internal/ui/config_int_test.go index d5545494..364916a1 100644 --- a/internal/ui/config_int_test.go +++ b/internal/ui/config_int_test.go @@ -10,12 +10,13 @@ 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" ) func Test_activeConfig(t *testing.T) { - assert.NoError(t, os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config")) - assert.NoError(t, config.InitLocs()) + require.NoError(t, os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config")) + require.NoError(t, config.InitLocs()) cl, ct := "cl-1", "ct-1-1" uu := map[string]struct { @@ -45,7 +46,7 @@ func Test_activeConfig(t *testing.T) { cfg := u.cfg if cfg.Config != nil { _, err := cfg.Config.K9s.ActivateContext(ct) - assert.NoError(t, err) + require.NoError(t, err) } cl, ct, ok := cfg.activeConfig() assert.Equal(t, u.ok, ok) diff --git a/internal/ui/config_test.go b/internal/ui/config_test.go index 95b71089..9bc34292 100644 --- a/internal/ui/config_test.go +++ b/internal/ui/config_test.go @@ -17,19 +17,20 @@ import ( "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/cli-runtime/pkg/genericclioptions" ) func TestSkinnedContext(t *testing.T) { - assert.NoError(t, os.Setenv(config.K9sEnvConfigDir, "/tmp/k9s-test")) - assert.NoError(t, config.InitLocs()) - defer assert.NoError(t, os.RemoveAll(config.K9sEnvConfigDir)) + require.NoError(t, os.Setenv(config.K9sEnvConfigDir, "/tmp/k9s-test")) + require.NoError(t, config.InitLocs()) + defer require.NoError(t, os.RemoveAll(config.K9sEnvConfigDir)) sf := filepath.Join("..", "config", "testdata", "skins", "black-and-wtf.yaml") raw, err := os.ReadFile(sf) - assert.NoError(t, err) + require.NoError(t, err) tf := filepath.Join(config.AppSkinsDir, "black-and-wtf.yaml") - assert.NoError(t, os.WriteFile(tf, raw, data.DefaultFileMod)) + require.NoError(t, os.WriteFile(tf, raw, data.DefaultFileMod)) var cfg ui.Configurator cfg.Config = mock.NewMockConfig() @@ -43,7 +44,7 @@ func TestSkinnedContext(t *testing.T) { mock.NewMockConnection(), mock.NewMockKubeSettings(&flags)) _, err = cfg.Config.K9s.ActivateContext("ct-1-1") - assert.NoError(t, err) + require.NoError(t, err) cfg.Config.K9s.UI = config.UI{Skin: "black-and-wtf"} cfg.RefreshStyles(newMockSynchronizer()) assert.True(t, cfg.HasSkin()) @@ -52,12 +53,12 @@ func TestSkinnedContext(t *testing.T) { } func TestBenchConfig(t *testing.T) { - assert.NoError(t, os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config")) - assert.NoError(t, config.InitLocs()) - defer assert.NoError(t, os.RemoveAll(config.K9sEnvConfigDir)) + require.NoError(t, os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config")) + require.NoError(t, config.InitLocs()) + defer require.NoError(t, os.RemoveAll(config.K9sEnvConfigDir)) - bc, error := config.EnsureBenchmarksCfgFile("cl-1", "ct-1") - assert.NoError(t, error) + bc, err := config.EnsureBenchmarksCfgFile("cl-1", "ct-1") + require.NoError(t, err) assert.Equal(t, "/tmp/test-config/clusters/cl-1/ct-1/benchmarks.yaml", bc) } @@ -69,10 +70,10 @@ func newMockSynchronizer() synchronizer { return synchronizer{} } -func (s synchronizer) Flash() *model.Flash { +func (synchronizer) Flash() *model.Flash { return model.NewFlash(100 * time.Millisecond) } -func (s synchronizer) Logo() *ui.Logo { return nil } -func (s synchronizer) UpdateClusterInfo() {} -func (s synchronizer) QueueUpdateDraw(func()) {} -func (s synchronizer) QueueUpdate(func()) {} +func (synchronizer) Logo() *ui.Logo { return nil } +func (synchronizer) UpdateClusterInfo() {} +func (synchronizer) QueueUpdateDraw(func()) {} +func (synchronizer) QueueUpdate(func()) {} diff --git a/internal/ui/crumbs.go b/internal/ui/crumbs.go index 3a757763..d9f5cac3 100644 --- a/internal/ui/crumbs.go +++ b/internal/ui/crumbs.go @@ -56,7 +56,7 @@ func (c *Crumbs) StackPopped(_, _ model.Component) { } // StackTop indicates the top of the stack. -func (c *Crumbs) StackTop(top model.Component) {} +func (*Crumbs) StackTop(model.Component) {} // Refresh updates view with new crumbs. func (c *Crumbs) refresh(crumbs []string) { diff --git a/internal/ui/crumbs_test.go b/internal/ui/crumbs_test.go index 57daa103..508e513d 100644 --- a/internal/ui/crumbs_test.go +++ b/internal/ui/crumbs_test.go @@ -52,7 +52,7 @@ func (c) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, return nil } func (c) SetRect(int, int, int, int) {} -func (c) GetRect() (int, int, int, int) { return 0, 0, 0, 0 } +func (c) GetRect() (a, b, c, d int) { return 0, 0, 0, 0 } func (c c) GetFocusable() tview.Focusable { return c } func (c) Focus(func(tview.Primitive)) {} func (c) Blur() {} diff --git a/internal/ui/deltas.go b/internal/ui/deltas.go index 5e4fdb7b..99419a0f 100644 --- a/internal/ui/deltas.go +++ b/internal/ui/deltas.go @@ -22,7 +22,7 @@ const ( MinusSign = "[green::b]โ†“" ) -var percent = regexp.MustCompile(`\A(\d+)\%\z`) +var percent = regexp.MustCompile(`\A(\d+)%\z`) func deltaNumb(o, n string) (string, bool) { var delta string diff --git a/internal/ui/dialog/confirm.go b/internal/ui/dialog/confirm.go index c2314376..bf451c1a 100644 --- a/internal/ui/dialog/confirm.go +++ b/internal/ui/dialog/confirm.go @@ -46,7 +46,7 @@ func ShowConfirmAck(app *ui.App, pages *ui.Pages, acceptStr string, override boo dismissConfirm(pages) cancel() }) - for i := 0; i < 2; i++ { + for i := range 2 { b := f.GetButton(i) if b == nil { continue @@ -67,7 +67,7 @@ func ShowConfirmAck(app *ui.App, pages *ui.Pages, acceptStr string, override boo } // ShowConfirm pops a confirmation dialog. -func ShowConfirm(styles config.Dialog, pages *ui.Pages, title, msg string, ack confirmFunc, cancel cancelFunc) { +func ShowConfirm(styles *config.Dialog, pages *ui.Pages, title, msg string, ack confirmFunc, cancel cancelFunc) { f := tview.NewForm(). SetItemPadding(0). SetButtonsAlign(tview.AlignCenter). @@ -85,7 +85,7 @@ func ShowConfirm(styles config.Dialog, pages *ui.Pages, title, msg string, ack c dismiss(pages) cancel() }) - for i := 0; i < 2; i++ { + for i := range 2 { if b := f.GetButton(i); b != nil { b.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()) b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color()) diff --git a/internal/ui/dialog/confirm_test.go b/internal/ui/dialog/confirm_test.go index 211e2091..41353cb6 100644 --- a/internal/ui/dialog/confirm_test.go +++ b/internal/ui/dialog/confirm_test.go @@ -16,14 +16,7 @@ func TestConfirmDialog(t *testing.T) { a := tview.NewApplication() p := ui.NewPages() a.SetRoot(p, false) - - ackFunc := func() { - assert.True(t, true) - } - caFunc := func() { - assert.True(t, true) - } - ShowConfirm(config.Dialog{}, p, "Blee", "Yo", ackFunc, caFunc) + ShowConfirm(new(config.Dialog), p, "Blee", "Yo", func() {}, func() {}) d := p.GetPrimitive(dialogKey).(*tview.ModalForm) assert.NotNil(t, d) diff --git a/internal/ui/dialog/delete.go b/internal/ui/dialog/delete.go index 686e95e6..62c98e8e 100644 --- a/internal/ui/dialog/delete.go +++ b/internal/ui/dialog/delete.go @@ -28,7 +28,7 @@ var propagationOptions []string = []string{ } // ShowDelete pops a resource deletion dialog. -func ShowDelete(styles config.Dialog, pages *ui.Pages, msg string, ok okFunc, cancel cancelFunc) { +func ShowDelete(styles *config.Dialog, pages *ui.Pages, msg string, ok okFunc, cancel cancelFunc) { propagation, force := "", false f := tview.NewForm() f.SetItemPadding(0) @@ -63,7 +63,7 @@ func ShowDelete(styles config.Dialog, pages *ui.Pages, msg string, ok okFunc, ca dismiss(pages) cancel() }) - for i := 0; i < 2; i++ { + for i := range 2 { b := f.GetButton(i) if b == nil { continue diff --git a/internal/ui/dialog/delete_test.go b/internal/ui/dialog/delete_test.go index 39b2eba0..5c31132d 100644 --- a/internal/ui/dialog/delete_test.go +++ b/internal/ui/dialog/delete_test.go @@ -20,10 +20,7 @@ func TestDeleteDialog(t *testing.T) { assert.Equal(t, propagationOptions[defaultPropagationIdx], p) assert.True(t, f) } - caFunc := func() { - assert.True(t, true) - } - ShowDelete(config.Dialog{}, p, "Yo", okFunc, caFunc) + ShowDelete(new(config.Dialog), p, "Yo", okFunc, func() {}) d := p.GetPrimitive(dialogKey).(*tview.ModalForm) assert.NotNil(t, d) diff --git a/internal/ui/dialog/error.go b/internal/ui/dialog/error.go index 1d81db5e..59198a63 100644 --- a/internal/ui/dialog/error.go +++ b/internal/ui/dialog/error.go @@ -14,7 +14,7 @@ import ( ) // ShowError pops an error dialog. -func ShowError(styles config.Dialog, pages *ui.Pages, msg string) { +func ShowError(styles *config.Dialog, pages *ui.Pages, msg string) { f := tview.NewForm() f.SetItemPadding(0) f.SetButtonsAlign(tview.AlignCenter). diff --git a/internal/ui/dialog/error_test.go b/internal/ui/dialog/error_test.go index b21fa4c6..2e0a4a73 100644 --- a/internal/ui/dialog/error_test.go +++ b/internal/ui/dialog/error_test.go @@ -15,7 +15,7 @@ import ( func TestErrorDialog(t *testing.T) { p := ui.NewPages() - ShowError(config.Dialog{}, p, "Yo") + ShowError(new(config.Dialog), p, "Yo") d := p.GetPrimitive(dialogKey).(*tview.ModalForm) assert.NotNil(t, d) diff --git a/internal/ui/dialog/prompt.go b/internal/ui/dialog/prompt.go index 8232d2d2..83f656b3 100644 --- a/internal/ui/dialog/prompt.go +++ b/internal/ui/dialog/prompt.go @@ -14,7 +14,7 @@ import ( type promptAction func(ctx context.Context) // ShowPrompt pops a prompt dialog. -func ShowPrompt(styles config.Dialog, pages *ui.Pages, title, msg string, action promptAction, cancel cancelFunc) { +func ShowPrompt(styles *config.Dialog, pages *ui.Pages, title, msg string, action promptAction, cancel cancelFunc) { f := tview.NewForm() f.SetItemPadding(0) f.SetButtonsAlign(tview.AlignCenter). @@ -31,7 +31,7 @@ func ShowPrompt(styles config.Dialog, pages *ui.Pages, title, msg string, action cancel() }) - for i := 0; i < f.GetButtonCount(); i++ { + for i := range f.GetButtonCount() { b := f.GetButton(i) if b == nil { continue diff --git a/internal/ui/dialog/prompt_test.go b/internal/ui/dialog/prompt_test.go index a5a7c022..4b7067d5 100644 --- a/internal/ui/dialog/prompt_test.go +++ b/internal/ui/dialog/prompt_test.go @@ -21,7 +21,7 @@ func TestShowPrompt(t *testing.T) { p := ui.NewPages() a.SetRoot(p, false) - ShowPrompt(config.Dialog{}, p, "Running", "Pod", func(context.Context) { + ShowPrompt(new(config.Dialog), p, "Running", "Pod", func(context.Context) { time.Sleep(time.Millisecond) }, func() { t.Errorf("unexpected cancellations") @@ -33,7 +33,7 @@ func TestShowPrompt(t *testing.T) { p := ui.NewPages() a.SetRoot(p, false) - go ShowPrompt(config.Dialog{}, p, "Running", "Pod", func(ctx context.Context) { + go ShowPrompt(new(config.Dialog), p, "Running", "Pod", func(ctx context.Context) { select { case <-time.After(time.Second): t.Errorf("expected cancellations") diff --git a/internal/ui/dialog/selection.go b/internal/ui/dialog/selection.go index 592105c4..ce0339d5 100644 --- a/internal/ui/dialog/selection.go +++ b/internal/ui/dialog/selection.go @@ -11,7 +11,7 @@ import ( type SelectAction func(index int) -func ShowSelection(styles config.Dialog, pages *ui.Pages, title string, options []string, action SelectAction) { +func ShowSelection(styles *config.Dialog, pages *ui.Pages, title string, options []string, action SelectAction) { list := tview.NewList() list.ShowSecondaryText(false) list.SetSelectedTextColor(styles.ButtonFocusFgColor.Color()) @@ -23,7 +23,7 @@ func ShowSelection(styles config.Dialog, pages *ui.Pages, title string, options } modal := ui.NewModalList("<"+title+">", list) - modal.SetDoneFunc(func(i int, s string) { + modal.SetDoneFunc(func(i int, _ string) { dismiss(pages) action(i) }) diff --git a/internal/ui/dialog/transfer.go b/internal/ui/dialog/transfer.go index 3141a894..767d21f7 100644 --- a/internal/ui/dialog/transfer.go +++ b/internal/ui/dialog/transfer.go @@ -31,7 +31,7 @@ type TransferDialogOpts struct { Cancel cancelFunc } -func ShowUploads(styles config.Dialog, pages *ui.Pages, opts TransferDialogOpts) { +func ShowUploads(styles *config.Dialog, pages *ui.Pages, opts *TransferDialogOpts) { f := tview.NewForm() f.SetItemPadding(0) f.SetButtonsAlign(tview.AlignCenter). @@ -98,7 +98,7 @@ func ShowUploads(styles config.Dialog, pages *ui.Pages, opts TransferDialogOpts) dismissConfirm(pages) opts.Cancel() }) - for i := 0; i < 2; i++ { + for i := range 2 { b := f.GetButton(i) if b == nil { continue diff --git a/internal/ui/flash.go b/internal/ui/flash.go index 0ba37d5b..6cfc273f 100644 --- a/internal/ui/flash.go +++ b/internal/ui/flash.go @@ -88,7 +88,7 @@ func (f *Flash) flashEmoji(l model.FlashLevel) string { if f.app.Config.K9s.UI.NoIcons { return "" } - // nolint:exhaustive + //nolint:exhaustive switch l { case model.FlashWarn: return emoDoh @@ -102,7 +102,7 @@ func (f *Flash) flashEmoji(l model.FlashLevel) string { // Helpers... func flashColor(l model.FlashLevel) tcell.Color { - // nolint:exhaustive + //nolint:exhaustive switch l { case model.FlashWarn: return tcell.ColorOrange diff --git a/internal/ui/indicator.go b/internal/ui/indicator.go index ed4645a4..82ee5c09 100644 --- a/internal/ui/indicator.go +++ b/internal/ui/indicator.go @@ -51,7 +51,7 @@ func (s *StatusIndicator) StylesChanged(styles *config.Styles) { const statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s[white::]::[darkturquoise::]%s" // ClusterInfoUpdated notifies the cluster meta was updated. -func (s *StatusIndicator) ClusterInfoUpdated(data model.ClusterMeta) { +func (s *StatusIndicator) ClusterInfoUpdated(data *model.ClusterMeta) { s.app.QueueUpdateDraw(func() { s.SetPermanent(fmt.Sprintf( statusIndicatorFmt, @@ -66,7 +66,7 @@ func (s *StatusIndicator) ClusterInfoUpdated(data model.ClusterMeta) { } // ClusterInfoChanged notifies the cluster meta was changed. -func (s *StatusIndicator) ClusterInfoChanged(prev, cur model.ClusterMeta) { +func (s *StatusIndicator) ClusterInfoChanged(prev, cur *model.ClusterMeta) { if !s.app.IsRunning() { return } diff --git a/internal/ui/logo_test.go b/internal/ui/logo_test.go index 168ef86e..22ba7f11 100644 --- a/internal/ui/logo_test.go +++ b/internal/ui/logo_test.go @@ -17,7 +17,7 @@ func TestNewLogoView(t *testing.T) { const elogo = "[#ffa500::b] ____ __ ________ \n[#ffa500::b]| |/ / __ \\______\n[#ffa500::b]| /\\____ / ___/\n[#ffa500::b]| \\ \\ / /\\___ \\\n[#ffa500::b]|____|\\__ \\/____//____ /\n[#ffa500::b] \\/ \\/ \n" assert.Equal(t, elogo, v.Logo().GetText(false)) - assert.Equal(t, "", v.Status().GetText(false)) + assert.Empty(t, v.Status().GetText(false)) } func TestLogoStatus(t *testing.T) { diff --git a/internal/ui/menu.go b/internal/ui/menu.go index b1b5a939..e74834bb 100644 --- a/internal/ui/menu.go +++ b/internal/ui/menu.go @@ -47,8 +47,8 @@ func NewMenu(styles *config.Styles) *Menu { func (m *Menu) StylesChanged(s *config.Styles) { m.styles = s m.SetBackgroundColor(s.BgColor()) - for row := 0; row < m.GetRowCount(); row++ { - for col := 0; col < m.GetColumnCount(); col++ { + for row := range m.GetRowCount() { + for col := range m.GetColumnCount() { if c := m.GetCell(row, col); c != nil { c.BackgroundColor = s.BgColor() } @@ -62,7 +62,7 @@ func (m *Menu) StackPushed(c model.Component) { } // StackPopped notifies a component was removed. -func (m *Menu) StackPopped(o, top model.Component) { +func (m *Menu) StackPopped(_, top model.Component) { if top != nil { m.HydrateMenu(top.Hints()) } else { @@ -85,15 +85,15 @@ func (m *Menu) HydrateMenu(hh model.MenuHints) { if m.hasDigits(hh) { colCount++ } - for row := 0; row < maxRows; row++ { + for row := range maxRows { table[row] = make(model.MenuHints, colCount) } t := m.buildMenuTable(hh, table, colCount) - for row := 0; row < len(t); row++ { - for col := 0; col < len(t[row]); col++ { + for row := range t { + for col := range len(t[row]) { c := tview.NewTableCell(t[row][col]) - if len(t[row][col]) == 0 { + if t[row][col] == "" { c = tview.NewTableCell("") } c.SetBackgroundColor(m.styles.BgColor()) @@ -102,7 +102,7 @@ func (m *Menu) HydrateMenu(hh model.MenuHints) { } } -func (m *Menu) hasDigits(hh model.MenuHints) bool { +func (*Menu) hasDigits(hh model.MenuHints) bool { for _, h := range hh { if !h.Visible { continue @@ -160,12 +160,13 @@ func (m *Menu) formatMenu(h model.MenuHint, size int) string { if h.Mnemonic == "" || h.Description == "" { return "" } + styles := m.styles.Frame() i, err := strconv.Atoi(h.Mnemonic) if err == nil { - return formatNSMenu(i, h.Description, m.styles.Frame()) + return formatNSMenu(i, h.Description, &styles) } - return formatPlainMenu(h, size, m.styles.Frame()) + return formatPlainMenu(h, size, &styles) } // ---------------------------------------------------------------------------- @@ -195,7 +196,7 @@ func ToMnemonic(s string) string { return "<" + keyConv(strings.ToLower(s)) + ">" } -func formatNSMenu(i int, name string, styles config.Frame) string { +func formatNSMenu(i int, name string, styles *config.Frame) string { fmat := strings.Replace(menuIndexFmt, "[key", "["+styles.Menu.NumKeyColor.String(), 1) fmat = strings.ReplaceAll(fmat, ":bg:", ":"+styles.Title.BgColor.String()+":") fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor.String(), 1) @@ -204,7 +205,7 @@ func formatNSMenu(i int, name string, styles config.Frame) string { return fmt.Sprintf(fmat, i, name) } -func formatPlainMenu(h model.MenuHint, size int, styles config.Frame) string { +func formatPlainMenu(h model.MenuHint, size int, styles *config.Frame) string { menuFmt := " [key:-:b]%-" + strconv.Itoa(size+2) + "s [fg:-:fgstyle]%s " fmat := strings.Replace(menuFmt, "[key", "["+styles.Menu.KeyColor.String(), 1) fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor.String(), 1) diff --git a/internal/ui/modal_list.go b/internal/ui/modal_list.go index ac331a9e..10ec39a6 100644 --- a/internal/ui/modal_list.go +++ b/internal/ui/modal_list.go @@ -52,7 +52,7 @@ func NewModalList(title string, list *tview.List) *ModalList { func (m *ModalList) Draw(screen tcell.Screen) { // Calculate the width of this modal. width := 0 - for i := 0; i < m.list.GetItemCount(); i++ { + for i := range m.list.GetItemCount() { main, secondary := m.list.GetItemText(i) width = max(width, len(main)+len(secondary)+2) } diff --git a/internal/ui/padding.go b/internal/ui/padding.go index 25cacdc5..d1fbfc62 100644 --- a/internal/ui/padding.go +++ b/internal/ui/padding.go @@ -40,7 +40,7 @@ func ComputeMaxColumns(pads MaxyPad, sortColName string, t *model1.TableData) { // IsASCII checks if table cell has all ascii characters. func IsASCII(s string) bool { - for i := 0; i < len(s); i++ { + for i := range s { if s[i] > unicode.MaxASCII { return false } diff --git a/internal/ui/padding_test.go b/internal/ui/padding_test.go index 0dbcb87a..99028941 100644 --- a/internal/ui/padding_test.go +++ b/internal/ui/padding_test.go @@ -144,7 +144,7 @@ func BenchmarkMaxColumn(b *testing.B) { b.ReportAllocs() b.ResetTimer() - for n := 0; n < b.N; n++ { + for range b.N { ComputeMaxColumns(pads, "A", table) } } diff --git a/internal/ui/pages.go b/internal/ui/pages.go index 919376ba..0b4b5138 100644 --- a/internal/ui/pages.go +++ b/internal/ui/pages.go @@ -87,7 +87,7 @@ func (p *Pages) StackPushed(c model.Component) { } // StackPopped notifies a component was removed. -func (p *Pages) StackPopped(o, top model.Component) { +func (p *Pages) StackPopped(o, _ model.Component) { p.delete(o) } diff --git a/internal/ui/prompt.go b/internal/ui/prompt.go index 0fcc88bb..5878dc75 100644 --- a/internal/ui/prompt.go +++ b/internal/ui/prompt.go @@ -145,7 +145,7 @@ func (p *Prompt) keyboard(evt *tcell.EventKey) *tcell.EventKey { return evt } - // nolint:exhaustive + //nolint:exhaustive switch evt.Key() { case tcell.KeyBackspace2, tcell.KeyBackspace, tcell.KeyDelete: p.model.Delete() @@ -279,7 +279,7 @@ func (p *Prompt) iconFor(k model.BufferKind) rune { return ' ' } - // nolint:exhaustive + //nolint:exhaustive switch k { case model.CommandBuffer: return '๐Ÿถ' @@ -292,7 +292,7 @@ func (p *Prompt) iconFor(k model.BufferKind) rune { // Helpers... func (p *Prompt) colorFor(k model.BufferKind) tcell.Color { - // nolint:exhaustive + //nolint:exhaustive switch k { case model.CommandBuffer: return p.styles.Prompt().Border.CommandColor.Color() diff --git a/internal/ui/prompt_test.go b/internal/ui/prompt_test.go index e29434ff..e6267c3d 100644 --- a/internal/ui/prompt_test.go +++ b/internal/ui/prompt_test.go @@ -99,7 +99,7 @@ func TestPromptColor(t *testing.T) { model.AddListener(prompt) model.SetActive(true) - assert.Equal(t, prompt.GetBorderColor(), testCase.expectedColor) + assert.Equal(t, testCase.expectedColor, prompt.GetBorderColor()) } } @@ -146,6 +146,6 @@ func TestPromptStyleChanged(t *testing.T) { prompt.StylesChanged(newStyles) model.SetActive(true) - assert.Equal(t, prompt.GetBorderColor(), testCase.expectedColor) + assert.Equal(t, testCase.expectedColor, prompt.GetBorderColor()) } } diff --git a/internal/ui/select_table.go b/internal/ui/select_table.go index 155ef371..ef1d75c5 100644 --- a/internal/ui/select_table.go +++ b/internal/ui/select_table.go @@ -107,8 +107,8 @@ func (s *SelectTable) SelectRow(r, c int, broadcast bool) { if !broadcast { s.SetSelectionChangedFunc(nil) } - if c := s.model.RowCount(); c > 0 && r-1 > c { - r = c + 1 + if count := s.model.RowCount(); count > 0 && r-1 > count { + r = count + 1 } defer s.SetSelectionChangedFunc(s.selectionChanged) s.Select(r, c) diff --git a/internal/ui/splash.go b/internal/ui/splash.go index 3fa2f131..74dc7a35 100644 --- a/internal/ui/splash.go +++ b/internal/ui/splash.go @@ -58,7 +58,7 @@ func NewSplash(styles *config.Styles, version string) *Splash { return &s } -func (s *Splash) layoutLogo(t *tview.TextView, styles *config.Styles) { +func (*Splash) layoutLogo(t *tview.TextView, styles *config.Styles) { logo := strings.Join(LogoBig, fmt.Sprintf("\n[%s::b]", styles.Body().LogoColor)) _, _ = fmt.Fprintf(t, "%s[%s::b]%s\n", strings.Repeat("\n", 2), @@ -66,6 +66,6 @@ func (s *Splash) layoutLogo(t *tview.TextView, styles *config.Styles) { logo) } -func (s *Splash) layoutRev(t *tview.TextView, rev string, styles *config.Styles) { +func (*Splash) layoutRev(t *tview.TextView, rev string, styles *config.Styles) { _, _ = fmt.Fprintf(t, "[%s::b]Revision [red::b]%s", styles.Body().FgColor, rev) } diff --git a/internal/ui/table.go b/internal/ui/table.go index 5f41915c..ac515007 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -37,7 +37,7 @@ type ( // Table represents tabular data. type Table struct { *SelectTable - gvr client.GVR + gvr *client.GVR sortCol model1.SortColumn manualSort bool Path string @@ -59,7 +59,7 @@ type Table struct { } // NewTable returns a new table view. -func NewTable(gvr client.GVR) *Table { +func NewTable(gvr *client.GVR) *Table { return &Table{ SelectTable: &SelectTable{ Table: tview.NewTable(), @@ -140,6 +140,7 @@ func (t *Table) SetViewSetting(vs *config.ViewSetting) bool { if !t.viewSetting.Equals(vs) { t.viewSetting = vs + slog.Debug("Updating custom view setting", slogs.GVR, t.gvr, slogs.ViewSetting, vs) t.model.SetViewSetting(t.ctx, vs) return true } @@ -178,7 +179,7 @@ func (t *Table) Init(ctx context.Context) { } // GVR returns a resource descriptor. -func (t *Table) GVR() client.GVR { return t.gvr } +func (t *Table) GVR() *client.GVR { return t.gvr } // ViewSettingsChanged notifies listener the view configuration changed. func (t *Table) ViewSettingsChanged(vs *config.ViewSetting) { @@ -250,7 +251,7 @@ func (t *Table) FilterInput(r rune) bool { } // Filter filters out table data. -func (t *Table) Filter(q string) { +func (t *Table) Filter(string) { t.ClearSelection() t.doUpdate(t.filtered(t.GetModel().Peek())) t.UpdateTitle() @@ -263,7 +264,7 @@ func (t *Table) Hints() model.MenuHints { } // ExtraHints returns additional hints. -func (t *Table) ExtraHints() map[string]string { +func (*Table) ExtraHints() map[string]string { return nil } @@ -433,7 +434,7 @@ func (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads M // SortColCmd designates a sorted column. func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { + return func(*tcell.EventKey) *tcell.EventKey { sc := t.getSortCol() sc.ASC = !sc.ASC if sc.Name != name { @@ -448,7 +449,7 @@ func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tce } // SortInvertCmd reverses sorting order. -func (t *Table) SortInvertCmd(evt *tcell.EventKey) *tcell.EventKey { +func (t *Table) SortInvertCmd(*tcell.EventKey) *tcell.EventKey { t.toggleSortCol() t.Refresh() @@ -500,7 +501,8 @@ func (t *Table) NameColIndex() int { func (t *Table) AddHeaderCell(col int, h model1.HeaderColumn) { sc := t.getSortCol() sortCol := h.Name == sc.Name - c := tview.NewTableCell(sortIndicator(sortCol, sc.ASC, t.styles.Table(), h.Name)) + styles := t.styles.Table() + c := tview.NewTableCell(sortIndicator(sortCol, sc.ASC, &styles, h.Name)) c.SetExpansion(1) c.SetSelectable(false) c.SetAlign(h.Align) @@ -523,7 +525,7 @@ func (t *Table) CmdBuff() *model.FishBuff { func (t *Table) ShowDeleted() { r, _ := t.GetSelection() cols := t.GetColumnCount() - for x := 0; x < cols; x++ { + for x := range cols { t.GetCell(r, x).SetAttributes(tcell.AttrDim) } } @@ -561,14 +563,14 @@ func (t *Table) styleTitle() string { resource = t.gvr.String() } - var title string + var ( + title string + styles = t.styles.Frame() + ) if ns == client.ClusterScope { - title = SkinTitle(fmt.Sprintf(TitleFmt, resource, render.AsThousands(rc)), t.styles.Frame()) + title = SkinTitle(fmt.Sprintf(TitleFmt, resource, render.AsThousands(rc)), &styles) } else { - title = SkinTitle(fmt.Sprintf(NSTitleFmt, resource, ns, render.AsThousands(rc)), t.styles.Frame()) - } - if ic := ROIndicator(t.readOnly, t.noIcon); ic != "" { - title = " " + ic + title + title = SkinTitle(fmt.Sprintf(NSTitleFmt, resource, ns, render.AsThousands(rc)), &styles) } buff := t.cmdBuff.GetText() @@ -582,7 +584,7 @@ func (t *Table) styleTitle() string { return title } - return title + SkinTitle(fmt.Sprintf(SearchFmt, buff), t.styles.Frame()) + return title + SkinTitle(fmt.Sprintf(SearchFmt, buff), &styles) } // ROIndicator returns an icon showing whether the session is in readonly mode or not. diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go index 04ed6462..9c783cac 100644 --- a/internal/ui/table_helper.go +++ b/internal/ui/table_helper.go @@ -67,7 +67,7 @@ func TrimLabelSelector(s string) string { } // SkinTitle decorates a title. -func SkinTitle(fmat string, style config.Frame) string { +func SkinTitle(fmat string, style *config.Frame) string { bgColor := style.Title.BgColor if bgColor == config.DefaultColor { bgColor = config.TransparentColor @@ -82,7 +82,7 @@ func SkinTitle(fmat string, style config.Frame) string { return fmat } -func sortIndicator(sort, asc bool, style config.Table, name string) string { +func sortIndicator(sort, asc bool, style *config.Table, name string) string { if !sort { return name } diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index b93c5507..1c86d459 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -70,35 +70,35 @@ type mockModel struct{} var _ ui.Tabular = &mockModel{} -func (t *mockModel) SetViewSetting(context.Context, *config.ViewSetting) {} -func (t *mockModel) SetInstance(string) {} -func (t *mockModel) SetLabelFilter(string) {} -func (t *mockModel) GetLabelFilter() string { return "" } -func (t *mockModel) Empty() bool { return false } -func (t *mockModel) RowCount() int { return 1 } -func (t *mockModel) HasMetrics() bool { return true } -func (t *mockModel) Peek() *model1.TableData { return makeTableData() } -func (t *mockModel) Refresh(context.Context) error { return nil } -func (t *mockModel) ClusterWide() bool { return false } -func (t *mockModel) GetNamespace() string { return "blee" } -func (t *mockModel) SetNamespace(string) {} -func (t *mockModel) ToggleToast() {} -func (t *mockModel) AddListener(model.TableListener) {} -func (t *mockModel) RemoveListener(model.TableListener) {} -func (t *mockModel) Watch(context.Context) error { return nil } -func (t *mockModel) Get(ctx context.Context, path string) (runtime.Object, error) { return nil, nil } -func (t *mockModel) InNamespace(string) bool { return true } -func (t *mockModel) SetRefreshRate(time.Duration) {} +func (*mockModel) SetViewSetting(context.Context, *config.ViewSetting) {} +func (*mockModel) SetInstance(string) {} +func (*mockModel) SetLabelFilter(string) {} +func (*mockModel) GetLabelFilter() string { return "" } +func (*mockModel) Empty() bool { return false } +func (*mockModel) RowCount() int { return 1 } +func (*mockModel) HasMetrics() bool { return true } +func (*mockModel) Peek() *model1.TableData { return makeTableData() } +func (*mockModel) Refresh(context.Context) error { return nil } +func (*mockModel) ClusterWide() bool { return false } +func (*mockModel) GetNamespace() string { return "blee" } +func (*mockModel) SetNamespace(string) {} +func (*mockModel) ToggleToast() {} +func (*mockModel) AddListener(model.TableListener) {} +func (*mockModel) RemoveListener(model.TableListener) {} +func (*mockModel) Watch(context.Context) error { return nil } +func (*mockModel) Get(context.Context, string) (runtime.Object, error) { return nil, nil } +func (*mockModel) InNamespace(string) bool { return true } +func (*mockModel) SetRefreshRate(time.Duration) {} -func (t *mockModel) Delete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error { +func (*mockModel) Delete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error { return nil } -func (t *mockModel) Describe(context.Context, string) (string, error) { +func (*mockModel) Describe(context.Context, string) (string, error) { return "", nil } -func (t *mockModel) ToYAML(ctx context.Context, path string) (string, error) { +func (*mockModel) ToYAML(context.Context, string) (string, error) { return "", nil } diff --git a/internal/ui/tree.go b/internal/ui/tree.go index 5af3046b..fd339cd4 100644 --- a/internal/ui/tree.go +++ b/internal/ui/tree.go @@ -37,7 +37,7 @@ func NewTree() *Tree { } // Init initializes the view. -func (t *Tree) Init(ctx context.Context) error { +func (t *Tree) Init(context.Context) error { t.BindKeys() t.SetBorder(true) t.SetBorderAttributes(tcell.AttrBold) @@ -85,7 +85,7 @@ func (t *Tree) Hints() model.MenuHints { } // ExtraHints returns additional hints. -func (t *Tree) ExtraHints() map[string]string { +func (*Tree) ExtraHints() map[string]string { return nil } @@ -105,11 +105,11 @@ func (t *Tree) keyboard(evt *tcell.EventKey) *tcell.EventKey { return evt } -func (t *Tree) noopCmd(evt *tcell.EventKey) *tcell.EventKey { +func (*Tree) noopCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } -func (t *Tree) toggleCollapseCmd(evt *tcell.EventKey) *tcell.EventKey { +func (t *Tree) toggleCollapseCmd(*tcell.EventKey) *tcell.EventKey { t.expandNodes = !t.expandNodes t.GetRoot().Walk(func(node, parent *tview.TreeNode) bool { if parent != nil { diff --git a/internal/view/actions.go b/internal/view/actions.go index b15bf7d1..01dd5628 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -14,6 +14,7 @@ import ( "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" "github.com/derailed/tcell/v2" + "k8s.io/apimachinery/pkg/util/sets" ) // AllScopes represents actions available for all views. @@ -21,9 +22,16 @@ const AllScopes = "all" // Runner represents a runnable action handler. type Runner interface { + // App returns the current app. App() *App + + // GetSelectedItem returns the current selected item. GetSelectedItem() string - Aliases() map[string]struct{} + + // Aliases returns all aliases assoxciated with the view GVR. + Aliases() sets.Set[string] + + // EnvFn returns the current environment function. EnvFn() EnvFunc } @@ -45,7 +53,7 @@ func includes(aliases []string, s string) bool { return false } -func inScope(scopes []string, aliases map[string]struct{}) bool { +func inScope(scopes []string, aliases sets.Set[string]) bool { if hasAll(scopes) { return true } @@ -107,7 +115,7 @@ func hotKeyActions(r Runner, aa *ui.KeyActions) error { } func gotoCmd(r Runner, cmd, path string, clearStack bool) ui.ActionHandler { - return func(evt *tcell.EventKey) *tcell.EventKey { + return func(*tcell.EventKey) *tcell.EventKey { r.App().gotoResource(cmd, path, clearStack, true) return nil } @@ -134,30 +142,30 @@ func pluginActions(r Runner, aa *ui.KeyActions) error { aliases = r.Aliases() ro = r.App().Config.IsReadOnly() ) - for k, plugin := range pp.Plugins { - if !inScope(plugin.Scopes, aliases) || (ro && plugin.Dangerous) { + for k := range pp.Plugins { + if !inScope(pp.Plugins[k].Scopes, aliases) || (ro && pp.Plugins[k].Dangerous) { continue } - - key, err := asKey(plugin.ShortCut) + key, err := asKey(pp.Plugins[k].ShortCut) if err != nil { errs = errors.Join(errs, err) continue } if _, ok := aa.Get(key); ok { - if !plugin.Override { - errs = errors.Join(errs, fmt.Errorf("duplicate plugin key found for %q in %q", plugin.ShortCut, k)) + if !pp.Plugins[k].Override { + errs = errors.Join(errs, fmt.Errorf("duplicate plugin key found for %q in %q", pp.Plugins[k].ShortCut, k)) continue } slog.Debug("Plugin overrode action shortcut", slogs.Plugin, k, - slogs.Key, plugin.ShortCut, + slogs.Key, pp.Plugins[k].ShortCut, ) } + plugin := pp.Plugins[k] aa.Add(key, ui.NewKeyActionWithOpts( - plugin.Description, - pluginAction(r, plugin), + pp.Plugins[k].Description, + pluginAction(r, &plugin), ui.ActionOpts{ Visible: true, Plugin: true, @@ -169,7 +177,7 @@ func pluginActions(r Runner, aa *ui.KeyActions) error { return errs } -func pluginAction(r Runner, p config.Plugin) ui.ActionHandler { +func pluginAction(r Runner, p *config.Plugin) ui.ActionHandler { return func(evt *tcell.EventKey) *tcell.EventKey { path := r.GetSelectedItem() if path == "" { @@ -196,7 +204,7 @@ func pluginAction(r Runner, p config.Plugin) ui.ActionHandler { pipes: p.Pipes, args: args, } - suspend, errChan, statusChan := run(r.App(), opts) + suspend, errChan, statusChan := run(r.App(), &opts) if !suspend { r.App().Flash().Infof("Plugin command failed: %q", p.Description) return @@ -220,11 +228,11 @@ func pluginAction(r Runner, p config.Plugin) ui.ActionHandler { } } }() - } if p.Confirm { msg := fmt.Sprintf("Run?\n%s %s", p.Command, strings.Join(args, " ")) - dialog.ShowConfirm(r.App().Styles.Dialog(), r.App().Content.Pages, "Confirm "+p.Description, msg, cb, func() {}) + d := r.App().Styles.Dialog() + dialog.ShowConfirm(&d, r.App().Content.Pages, "Confirm "+p.Description, msg, cb, func() {}) return nil } cb() diff --git a/internal/view/actions_test.go b/internal/view/actions_test.go index 051927fc..b64e446c 100644 --- a/internal/view/actions_test.go +++ b/internal/view/actions_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/sets" ) func init() { @@ -19,13 +20,19 @@ func TestHasAll(t *testing.T) { scopes []string e bool }{ - "empty": {}, - "all": {scopes: []string{"blee", "duh", AllScopes}, e: true}, - "no-all": {scopes: []string{"blee", "duh", "alla"}}, + "empty": {}, + + "all": { + scopes: []string{"blee", "duh", AllScopes}, + e: true, + }, + + "none": { + scopes: []string{"blee", "duh", "alla"}, + }, } - for k := range uu { - u := uu[k] + for k, u := range uu { t.Run(k, func(t *testing.T) { assert.Equal(t, u.e, hasAll(u.scopes)) }) @@ -39,12 +46,20 @@ func TestIncludes(t *testing.T) { e bool }{ "empty": {}, - "yes": {s: "blee", ss: []string{"yo", "duh", "blee"}, e: true}, - "no": {s: "blue", ss: []string{"yo", "duh", "blee"}}, + + "yes": { + s: "blee", + ss: []string{"yo", "duh", "blee"}, + e: true, + }, + + "no": { + s: "blue", + ss: []string{"yo", "duh", "blee"}, + }, } - for k := range uu { - u := uu[k] + for k, u := range uu { t.Run(k, func(t *testing.T) { assert.Equal(t, u.e, includes(u.ss, u.s)) }) @@ -54,19 +69,38 @@ func TestIncludes(t *testing.T) { func TestInScope(t *testing.T) { uu := map[string]struct { ss []string - aa map[string]struct{} + aa sets.Set[string] e bool }{ - "empty": {}, - "yes": {e: true, ss: []string{"blee", "duh", "fred"}, aa: map[string]struct{}{"blee": {}, "fred": {}, "duh": {}}}, - "no": {ss: []string{"blee", "duh", "fred"}, aa: map[string]struct{}{"blee1": {}, "fred1": {}}}, - "empty scopes": {aa: map[string]struct{}{"blee1": {}, "fred1": {}}}, - "empty aliases": {ss: []string{"blee1", "fred1"}}, - "all": {e: true, ss: []string{AllScopes}, aa: map[string]struct{}{"blee1": {}, "fred1": {}}}, + "empty": {}, + + "yes": { + e: true, + ss: []string{"blee", "duh", "fred"}, + aa: sets.New("blee", "fred", "duh"), + }, + + "no": { + ss: []string{"blee", "duh", "fred"}, + aa: sets.New("blee1", "fred1"), + }, + + "no-scopes": { + aa: sets.New("aa", "blee1", "fred1"), + }, + + "no-aliases": { + ss: []string{"blee1", "fred1"}, + }, + + "all": { + e: true, + ss: []string{AllScopes}, + aa: sets.New("blee1", "fred1"), + }, } - for k := range uu { - u := uu[k] + for k, u := range uu { t.Run(k, func(t *testing.T) { assert.Equal(t, u.e, inScope(u.ss, u.aa)) }) diff --git a/internal/view/alias.go b/internal/view/alias.go index a8a99dfe..a676572b 100644 --- a/internal/view/alias.go +++ b/internal/view/alias.go @@ -20,7 +20,7 @@ type Alias struct { } // NewAlias returns a new alias view. -func NewAlias(gvr client.GVR) ResourceViewer { +func NewAlias(gvr *client.GVR) ResourceViewer { a := Alias{ ResourceViewer: NewBrowser(gvr), } @@ -66,8 +66,7 @@ func (a *Alias) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { if path == "" { return evt } - gvr := client.NewGVR(path) - a.App().gotoResource(gvr.String(), "", true, true) + a.App().gotoResource(client.NewGVR(path).String(), "", true, true) return nil } diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go index ad16d412..06a611a6 100644 --- a/internal/view/alias_test.go +++ b/internal/view/alias_test.go @@ -19,21 +19,22 @@ import ( "github.com/derailed/k9s/internal/view" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) func TestAliasNew(t *testing.T) { - v := view.NewAlias(client.NewGVR("aliases")) + v := view.NewAlias(client.AliGVR) - assert.Nil(t, v.Init(makeContext())) + require.NoError(t, v.Init(makeContext())) assert.Equal(t, "Aliases", v.Name()) - assert.Equal(t, 6, len(v.Hints())) + assert.Len(t, v.Hints(), 6) } func TestAliasSearch(t *testing.T) { - v := view.NewAlias(client.NewGVR("aliases")) - assert.Nil(t, v.Init(makeContext())) + v := view.NewAlias(client.AliGVR) + require.NoError(t, v.Init(makeContext())) v.GetTable().SetModel(&mockModel{}) v.GetTable().Refresh() v.App().Prompt().SetModel(v.GetTable().CmdBuff()) @@ -44,8 +45,8 @@ func TestAliasSearch(t *testing.T) { } func TestAliasGoto(t *testing.T) { - v := view.NewAlias(client.NewGVR("aliases")) - assert.Nil(t, v.Init(makeContext())) + v := view.NewAlias(client.AliGVR) + require.NoError(t, v.Init(makeContext())) v.GetTable().Select(0, 0) b := buffL{} @@ -64,12 +65,12 @@ type buffL struct { changed int } -func (b *buffL) BufferChanged(_, _ string) { +func (b *buffL) BufferChanged(string, string) { b.changed++ } -func (b *buffL) BufferCompleted(_, _ string) {} +func (*buffL) BufferCompleted(string, string) {} -func (b *buffL) BufferActive(state bool, kind model.BufferKind) { +func (b *buffL) BufferActive(bool, model.BufferKind) { b.active++ } @@ -86,44 +87,44 @@ var ( _ ui.Suggester = (*mockModel)(nil) ) -func (t *mockModel) SetViewSetting(context.Context, *config.ViewSetting) {} -func (t *mockModel) CurrentSuggestion() (string, bool) { return "", false } -func (t *mockModel) NextSuggestion() (string, bool) { return "", false } -func (t *mockModel) PrevSuggestion() (string, bool) { return "", false } -func (t *mockModel) ClearSuggestions() {} -func (t *mockModel) SetInstance(string) {} -func (t *mockModel) SetLabelFilter(string) {} -func (t *mockModel) GetLabelFilter() string { return "" } -func (t *mockModel) Empty() bool { return false } -func (t *mockModel) RowCount() int { return 1 } -func (t *mockModel) HasMetrics() bool { return true } -func (t *mockModel) Peek() *model1.TableData { return makeTableData() } -func (t *mockModel) ClusterWide() bool { return false } -func (t *mockModel) GetNamespace() string { return "blee" } -func (t *mockModel) SetNamespace(string) {} -func (t *mockModel) ToggleToast() {} -func (t *mockModel) AddListener(model.TableListener) {} -func (t *mockModel) RemoveListener(model.TableListener) {} -func (t *mockModel) Watch(context.Context) error { return nil } -func (t *mockModel) Refresh(context.Context) error { return nil } -func (t *mockModel) Get(context.Context, string) (runtime.Object, error) { +func (*mockModel) SetViewSetting(context.Context, *config.ViewSetting) {} +func (*mockModel) CurrentSuggestion() (string, bool) { return "", false } +func (*mockModel) NextSuggestion() (string, bool) { return "", false } +func (*mockModel) PrevSuggestion() (string, bool) { return "", false } +func (*mockModel) ClearSuggestions() {} +func (*mockModel) SetInstance(string) {} +func (*mockModel) SetLabelFilter(string) {} +func (*mockModel) GetLabelFilter() string { return "" } +func (*mockModel) Empty() bool { return false } +func (*mockModel) RowCount() int { return 1 } +func (*mockModel) HasMetrics() bool { return true } +func (*mockModel) Peek() *model1.TableData { return makeTableData() } +func (*mockModel) ClusterWide() bool { return false } +func (*mockModel) GetNamespace() string { return "blee" } +func (*mockModel) SetNamespace(string) {} +func (*mockModel) ToggleToast() {} +func (*mockModel) AddListener(model.TableListener) {} +func (*mockModel) RemoveListener(model.TableListener) {} +func (*mockModel) Watch(context.Context) error { return nil } +func (*mockModel) Refresh(context.Context) error { return nil } +func (*mockModel) Get(context.Context, string) (runtime.Object, error) { return nil, nil } -func (t *mockModel) Delete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error { +func (*mockModel) Delete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error { return nil } -func (t *mockModel) Describe(context.Context, string) (string, error) { +func (*mockModel) Describe(context.Context, string) (string, error) { return "", nil } -func (t *mockModel) ToYAML(ctx context.Context, path string) (string, error) { +func (*mockModel) ToYAML(context.Context, string) (string, error) { return "", nil } -func (t *mockModel) InNamespace(string) bool { return true } -func (t *mockModel) SetRefreshRate(time.Duration) {} +func (*mockModel) InNamespace(string) bool { return true } +func (*mockModel) SetRefreshRate(time.Duration) {} func makeTableData() *model1.TableData { return model1.NewTableDataWithRows( diff --git a/internal/view/app.go b/internal/view/app.go index aa2b6659..8735b41c 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "log/slog" + "maps" "os" "os/signal" "runtime" @@ -93,7 +94,7 @@ func (a *App) ConOK() bool { } // Init initializes the application. -func (a *App) Init(version string, rate int) error { +func (a *App) Init(version string, _ int) error { a.version = model.NormalizeVersion(version) ctx := context.WithValue(context.Background(), internal.KeyApp, a) @@ -139,7 +140,7 @@ func (a *App) Init(version string, rate int) error { return nil } -func (a *App) stopImgScanner() { +func (*App) stopImgScanner() { if vul.ImgScanner != nil { vul.ImgScanner.Stop() } @@ -173,7 +174,7 @@ func (a *App) layout(ctx context.Context) { } } -func (a *App) initSignals() { +func (*App) initSignals() { sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGHUP) @@ -198,8 +199,8 @@ func (a *App) suggestCommand() model.SuggestionFunc { } ls := strings.ToLower(s) - for _, k := range a.command.alias.Keys() { - if suggest, ok := cmd.ShouldAddSuggest(ls, k); ok { + for alias := range maps.Keys(a.command.alias.Alias) { + if suggest, ok := cmd.ShouldAddSuggest(ls, alias); ok { entries = append(entries, suggest) } } @@ -253,7 +254,7 @@ func (a *App) bindKeys() { })) } -func (a *App) dumpGOR(evt *tcell.EventKey) *tcell.EventKey { +func (*App) dumpGOR(evt *tcell.EventKey) *tcell.EventKey { slog.Debug("GOR", slogs.GOR, runtime.NumGoroutine()) bb := make([]byte, 5_000_000) runtime.Stack(bb, true) @@ -401,7 +402,7 @@ func (a *App) refreshCluster(context.Context) error { c.Stop() } - count, maxConnRetry := atomic.LoadInt32(&a.conRetry), int32(a.Config.K9s.MaxConnRetry) + count, maxConnRetry := atomic.LoadInt32(&a.conRetry), a.Config.K9s.MaxConnRetry if count >= maxConnRetry { slog.Error("Conn check failed. Bailing out!", slogs.Retry, count, @@ -601,7 +602,7 @@ func (a *App) setIndicator(l model.FlashLevel, msg string) { } // PrevCmd pops the command stack. -func (a *App) PrevCmd(evt *tcell.EventKey) *tcell.EventKey { +func (a *App) PrevCmd(*tcell.EventKey) *tcell.EventKey { if !a.Content.IsLast() { a.Content.Pop() } @@ -646,7 +647,8 @@ func (a *App) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { } func (a *App) cowCmd(msg string) { - dialog.ShowError(a.Styles.Dialog(), a.Content.Pages, msg) + d := a.Styles.Dialog() + dialog.ShowError(&d, a.Content.Pages, msg) } func (a *App) dirCmd(path string, pushCmd bool) error { @@ -739,30 +741,31 @@ func (a *App) lastCommand(evt *tcell.EventKey) *tcell.EventKey { if len(cmds) < 1 { a.App.Flash().Warn("No previous view to switch to") return evt - } else { - a.cmdHistory.Last() - a.gotoResource(cmds[a.cmdHistory.CurrentIndex()], "", true, false) } + a.cmdHistory.Last() + a.gotoResource(cmds[a.cmdHistory.CurrentIndex()], "", true, false) + return nil } -func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { +func (a *App) aliasCmd(*tcell.EventKey) *tcell.EventKey { if a.Content.Top() != nil && a.Content.Top().Name() == aliasTitle { a.Content.Pop() return nil } - if err := a.inject(NewAlias(client.NewGVR("aliases")), false); err != nil { + if err := a.inject(NewAlias(client.AliGVR), false); err != nil { a.Flash().Err(err) } return nil } -func (a *App) gotoResource(c, path string, clearStack bool, pushCmd bool) { +func (a *App) gotoResource(c, path string, clearStack, pushCmd bool) { err := a.command.run(cmd.NewInterpreter(c), path, clearStack, pushCmd) if err != nil { - dialog.ShowError(a.Styles.Dialog(), a.Content.Pages, err.Error()) + d := a.Styles.Dialog() + dialog.ShowError(&d, a.Content.Pages, err.Error()) } } diff --git a/internal/view/benchmark.go b/internal/view/benchmark.go index 386241e7..e7043917 100644 --- a/internal/view/benchmark.go +++ b/internal/view/benchmark.go @@ -26,7 +26,7 @@ type Benchmark struct { } // NewBenchmark returns a new viewer. -func NewBenchmark(gvr client.GVR) ResourceViewer { +func NewBenchmark(gvr *client.GVR) ResourceViewer { b := Benchmark{ ResourceViewer: NewBrowser(gvr), } @@ -43,7 +43,7 @@ func (b *Benchmark) benchContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyDir, benchDir(b.App().Config)) } -func (b *Benchmark) viewBench(app *App, model ui.Tabular, gvr client.GVR, path string) { +func (b *Benchmark) viewBench(app *App, _ ui.Tabular, _ *client.GVR, path string) { data, err := readBenchFile(app.Config, b.benchFile()) if err != nil { app.Flash().Errf("Unable to load bench file %s", err) diff --git a/internal/view/browser.go b/internal/view/browser.go index 64667c58..92dd4009 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -25,6 +25,7 @@ import ( "github.com/derailed/k9s/internal/view/cmd" "github.com/derailed/tcell/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" ) // Browser represents a generic resource browser. @@ -32,7 +33,7 @@ type Browser struct { *Table namespaces map[int]string - meta metav1.APIResource + meta *metav1.APIResource accessor dao.Accessor contextFn ContextFunc cancelFn context.CancelFunc @@ -41,7 +42,7 @@ type Browser struct { } // NewBrowser returns a new browser. -func NewBrowser(gvr client.GVR) ResourceViewer { +func NewBrowser(gvr *client.GVR) ResourceViewer { return &Browser{ Table: NewTable(gvr), } @@ -62,7 +63,6 @@ func (b *Browser) getUpdating() bool { // SetCommand sets the current command. func (b *Browser) SetCommand(cmd *cmd.Interpreter) { b.GetTable().SetCommand(cmd) - //b.Table.SetViewSetting(b.app.CustomView().VSFor(cmd) } // Init watches all running pods in given namespace. @@ -79,12 +79,12 @@ func (b *Browser) Init(ctx context.Context) error { } b.GetTable().SetColorerFn(colorerFn) - if err = b.Table.Init(ctx); err != nil { - return err + if e := b.Table.Init(ctx); e != nil { + return e } ns := client.CleanseNamespace(b.app.Config.ActiveNamespace()) if dao.IsK8sMeta(b.meta) && b.app.ConOK() { - if _, e := b.app.factory.CanForResource(ns, b.GVR().String(), client.ListAccess); e != nil { + if _, e := b.app.factory.CanForResource(ns, b.GVR(), client.ListAccess); e != nil { return e } } @@ -178,11 +178,9 @@ func (b *Browser) Start() { // Stop terminates browser updates. func (b *Browser) Stop() { b.mx.Lock() - { - if b.cancelFn != nil { - b.cancelFn() - b.cancelFn = nil - } + if b.cancelFn != nil { + b.cancelFn() + b.cancelFn = nil } b.mx.Unlock() b.GetModel().RemoveListener(b) @@ -200,7 +198,7 @@ func (b *Browser) SetLabelFilter(labels map[string]string) { } // BufferChanged indicates the buffer was changed. -func (b *Browser) BufferChanged(_, _ string) {} +func (*Browser) BufferChanged(_, _ string) {} // BufferCompleted indicates input was accepted. func (b *Browser) BufferCompleted(text, _ string) { @@ -212,7 +210,7 @@ func (b *Browser) BufferCompleted(text, _ string) { } // BufferActive indicates the buff activity changed. -func (b *Browser) BufferActive(state bool, k model.BufferKind) { +func (b *Browser) BufferActive(state bool, _ model.BufferKind) { if state { return } @@ -234,7 +232,6 @@ func (b *Browser) BufferActive(state bool, k model.BufferKind) { if b.GetRowCount() > 1 { b.App().filterHistory.Push(b.CmdBuff().GetText()) } - }) } @@ -242,12 +239,10 @@ func (b *Browser) prepareContext() context.Context { ctx := b.defaultContext() b.mx.Lock() - { - if b.cancelFn != nil { - b.cancelFn() - } - ctx, b.cancelFn = context.WithCancel(ctx) + if b.cancelFn != nil { + b.cancelFn() } + ctx, b.cancelFn = context.WithCancel(ctx) b.mx.Unlock() if b.contextFn != nil { @@ -257,9 +252,7 @@ func (b *Browser) prepareContext() context.Context { b.Path = path } b.mx.Lock() - { - b.SetContext(ctx) - } + b.SetContext(ctx) b.mx.Unlock() return ctx @@ -279,8 +272,8 @@ func (b *Browser) SetContextFn(f ContextFunc) { b.contextFn = f } func (b *Browser) GetTable() *Table { return b.Table } // Aliases returns all available aliases. -func (b *Browser) Aliases() map[string]struct{} { - return aliasesFor(b.meta, b.app.command.AliasesFor(b.meta.Name)) +func (b *Browser) Aliases() sets.Set[string] { + return aliases(b.meta, b.app.command.AliasesFor(client.NewGVRFromMeta(b.meta))) } // ---------------------------------------------------------------------------- @@ -446,7 +439,7 @@ func (b *Browser) editCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func editRes(app *App, gvr client.GVR, path string) error { +func editRes(app *App, gvr *client.GVR, path string) error { if path == "" { return fmt.Errorf("nothing selected %q", path) } @@ -454,20 +447,19 @@ func editRes(app *App, gvr client.GVR, path string) error { if client.IsClusterScoped(ns) { ns = client.BlankNamespace } - if gvr.String() == "v1/namespaces" { + if gvr == client.NsGVR { ns = n } - if ok, err := app.Conn().CanI(ns, gvr.String(), n, client.PatchAccess); !ok || err != nil { + if ok, err := app.Conn().CanI(ns, gvr, n, client.PatchAccess); !ok || err != nil { return fmt.Errorf("current user can't edit resource %s", gvr) } args := make([]string, 0, 10) - args = append(args, "edit") - args = append(args, gvr.FQN(n)) + args = append(args, "edit", gvr.FQN(n)) if ns != client.BlankNamespace { args = append(args, "-n", ns) } - if err := runK(app, shellOpts{clear: true, args: args}); err != nil { + if err := runK(app, &shellOpts{clear: true, args: args}); err != nil { app.Flash().Errf("Edit command failed: %s", err) } @@ -482,7 +474,7 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { } ns := b.namespaces[i] - auth, err := b.App().factory.Client().CanI(ns, b.GVR().String(), "", client.ListAccess) + auth, err := b.App().factory.Client().CanI(ns, b.GVR(), "", client.ListAccess) if !auth { if err == nil { err = fmt.Errorf("current user can't access namespace %s", ns) @@ -617,7 +609,8 @@ func (b *Browser) namespaceActions(aa *ui.KeyActions) { } func (b *Browser) simpleDelete(selections []string, msg string) { - dialog.ShowConfirm(b.app.Styles.Dialog(), b.app.Content.Pages, "Confirm Delete", msg, func() { + d := b.app.Styles.Dialog() + dialog.ShowConfirm(&d, b.app.Content.Pages, "Confirm Delete", msg, func() { b.ShowDeleted() if len(selections) > 1 { b.app.Flash().Infof("Delete %d marked %s", len(selections), b.GVR().R()) @@ -663,5 +656,6 @@ func (b *Browser) resourceDelete(selections []string, msg string) { } b.refresh() } - dialog.ShowDelete(b.app.Styles.Dialog(), b.app.Content.Pages, msg, okFn, func() {}) + d := b.app.Styles.Dialog() + dialog.ShowDelete(&d, b.app.Content.Pages, msg, okFn, func() {}) } diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go index f11c541e..314aa566 100644 --- a/internal/view/cluster_info.go +++ b/internal/view/cluster_info.go @@ -54,7 +54,7 @@ func (c *ClusterInfo) StylesChanged(s *config.Styles) { func (c *ClusterInfo) hasMetrics() bool { mx := c.app.Conn().HasMetrics() if mx { - auth, err := c.app.Conn().CanI("", "metrics.k8s.io/v1beta1/nodes", "", client.ListAccess) + auth, err := c.app.Conn().CanI("", client.NmxGVR, "", client.ListAccess) if err != nil { slog.Warn("No nodes metrics access", slogs.Error, err) } @@ -97,11 +97,11 @@ func (c *ClusterInfo) setCell(row int, s string) int { } // ClusterInfoUpdated notifies the cluster meta was updated. -func (c *ClusterInfo) ClusterInfoUpdated(data model.ClusterMeta) { +func (c *ClusterInfo) ClusterInfoUpdated(data *model.ClusterMeta) { c.ClusterInfoChanged(data, data) } -func (c *ClusterInfo) warnCell(s string, w bool) string { +func (*ClusterInfo) warnCell(s string, w bool) string { if w { return fmt.Sprintf("[orangered::b]%s", s) } @@ -110,11 +110,16 @@ func (c *ClusterInfo) warnCell(s string, w bool) string { } // ClusterInfoChanged notifies the cluster meta was changed. -func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) { +func (c *ClusterInfo) ClusterInfoChanged(prev, curr *model.ClusterMeta) { c.app.QueueUpdateDraw(func() { c.Clear() c.layout() - row := c.setCell(0, curr.Context) + + context := curr.Context + if ic := ui.ROIndicator(c.app.Config.IsReadOnly(), c.app.Config.K9s.UI.NoIcons); ic != "" { + context += " " + ic + } + row := c.setCell(0, context) row = c.setCell(row, curr.Cluster) row = c.setCell(row, curr.User) if curr.K9sLatest != "" { @@ -139,12 +144,12 @@ const defconFmt = "%s %s level!" func (c *ClusterInfo) setDefCon(cpu, mem int) { var set bool - l := c.app.Config.K9s.Thresholds.LevelFor("cpu", cpu) + l := c.app.Config.K9s.Thresholds.LevelFor(config.CPU, cpu) if l > config.SeverityLow { c.app.Status(flashLevel(l), fmt.Sprintf(defconFmt, flashMessage(l), "CPU")) set = true } - l = c.app.Config.K9s.Thresholds.LevelFor("memory", mem) + l = c.app.Config.K9s.Thresholds.LevelFor(config.MEM, mem) if l > config.SeverityLow { c.app.Status(flashLevel(l), fmt.Sprintf(defconFmt, flashMessage(l), "Memory")) set = true @@ -155,7 +160,7 @@ func (c *ClusterInfo) setDefCon(cpu, mem int) { } func (c *ClusterInfo) updateStyle() { - for row := 0; row < c.GetRowCount(); row++ { + for row := range c.GetRowCount() { c.GetCell(row, 0).SetTextColor(c.styles.K9s.Info.FgColor.Color()) c.GetCell(row, 0).SetBackgroundColor(c.styles.BgColor()) var s tcell.Style @@ -170,7 +175,7 @@ func (c *ClusterInfo) updateStyle() { // Helpers... func flashLevel(l config.SeverityLevel) model.FlashLevel { - // nolint:exhaustive + //nolint:exhaustive switch l { case config.SeverityHigh: return model.FlashErr @@ -182,7 +187,7 @@ func flashLevel(l config.SeverityLevel) model.FlashLevel { } func flashMessage(l config.SeverityLevel) string { - // nolint:exhaustive + //nolint:exhaustive switch l { case config.SeverityHigh: return "Critical" diff --git a/internal/view/cm.go b/internal/view/cm.go index 3a18477d..f7850df1 100644 --- a/internal/view/cm.go +++ b/internal/view/cm.go @@ -19,7 +19,7 @@ type ConfigMap struct { } // NewConfigMap returns a new viewer. -func NewConfigMap(gvr client.GVR) ResourceViewer { +func NewConfigMap(gvr *client.GVR) ResourceViewer { s := ConfigMap{ ResourceViewer: NewOwnerExtender( NewBrowser(gvr), @@ -35,10 +35,10 @@ func (s *ConfigMap) bindKeys(aa *ui.KeyActions) { } func (s *ConfigMap) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanRefs(evt, s.App(), s.GetTable(), dao.CmGVR) + return scanRefs(evt, s.App(), s.GetTable(), client.CmGVR) } -func scanRefs(evt *tcell.EventKey, a *App, t *Table, gvr client.GVR) *tcell.EventKey { +func scanRefs(evt *tcell.EventKey, a *App, t *Table, gvr *client.GVR) *tcell.EventKey { path := t.GetSelectedItem() if path == "" { return evt @@ -55,7 +55,7 @@ func scanRefs(evt *tcell.EventKey, a *App, t *Table, gvr client.GVR) *tcell.Even return nil } a.Flash().Infof("Viewing references for %s::%s", gvr, path) - view := NewReference(client.NewGVR("references")) + view := NewReference(client.RefGVR) view.SetContextFn(refContext(gvr, path, false)) if err := a.inject(view, false); err != nil { a.Flash().Err(err) @@ -64,7 +64,7 @@ func scanRefs(evt *tcell.EventKey, a *App, t *Table, gvr client.GVR) *tcell.Even return nil } -func refContext(gvr client.GVR, path string, wait bool) ContextFunc { +func refContext(gvr *client.GVR, path string, wait bool) ContextFunc { return func(ctx context.Context) context.Context { ctx = context.WithValue(ctx, internal.KeyPath, path) ctx = context.WithValue(ctx, internal.KeyGVR, gvr) diff --git a/internal/view/cm_test.go b/internal/view/cm_test.go index 6e787def..158484f8 100644 --- a/internal/view/cm_test.go +++ b/internal/view/cm_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestConfigMapNew(t *testing.T) { - s := view.NewConfigMap(client.NewGVR("v1/configmaps")) + s := view.NewConfigMap(client.CmGVR) - assert.Nil(t, s.Init(makeCtx())) + require.NoError(t, s.Init(makeCtx())) assert.Equal(t, "ConfigMaps", s.Name()) - assert.Equal(t, 7, len(s.Hints())) + assert.Len(t, s.Hints(), 7) } diff --git a/internal/view/cmd/args.go b/internal/view/cmd/args.go index 7f8a8f3b..5aad75d1 100644 --- a/internal/view/cmd/args.go +++ b/internal/view/cmd/args.go @@ -19,61 +19,62 @@ const ( type args map[string]string func newArgs(p *Interpreter, aa []string) args { - args := make(args, len(aa)) + arguments := make(args, len(aa)) if len(aa) == 0 { - return args + return arguments } for i := 0; i < len(aa); i++ { a := strings.TrimSpace(aa[i]) switch { case strings.Index(a, contextFlag) == 0: - args[contextKey] = a[1:] + arguments[contextKey] = a[1:] case strings.Index(a, fuzzyFlag) == 0: if a == fuzzyFlag { - if i++; i < len(aa) { - args[fuzzyKey] = strings.ToLower(strings.TrimSpace(aa[i])) + i++ + if i < len(aa) { + arguments[fuzzyKey] = strings.ToLower(strings.TrimSpace(aa[i])) } } else { - args[fuzzyKey] = strings.ToLower(a[2:]) + arguments[fuzzyKey] = strings.ToLower(a[2:]) } case strings.Index(a, filterFlag) == 0: if p.IsDirCmd() { - if _, ok := args[topicKey]; !ok { - args[topicKey] = a + if _, ok := arguments[topicKey]; !ok { + arguments[topicKey] = a } } else { - args[filterKey] = strings.ToLower(a[1:]) + arguments[filterKey] = strings.ToLower(a[1:]) } case strings.Contains(a, labelFlag): if ll := ToLabels(a); len(ll) != 0 { - args[labelKey] = strings.ToLower(a) + arguments[labelKey] = strings.ToLower(a) } default: switch { case p.IsContextCmd(): - args[contextKey] = a + arguments[contextKey] = a case p.IsDirCmd(): - if _, ok := args[topicKey]; !ok { - args[topicKey] = a + if _, ok := arguments[topicKey]; !ok { + arguments[topicKey] = a } case p.IsXrayCmd(): - if _, ok := args[topicKey]; ok { - args[nsKey] = strings.ToLower(a) + if _, ok := arguments[topicKey]; ok { + arguments[nsKey] = strings.ToLower(a) } else { - args[topicKey] = strings.ToLower(a) + arguments[topicKey] = strings.ToLower(a) } default: - args[nsKey] = strings.ToLower(a) + arguments[nsKey] = strings.ToLower(a) } } } - return args + return arguments } func (a args) hasFilters() bool { diff --git a/internal/view/cmd/args_test.go b/internal/view/cmd/args_test.go index 157d6f60..962a24bb 100644 --- a/internal/view/cmd/args_test.go +++ b/internal/view/cmd/args_test.go @@ -121,7 +121,6 @@ func TestFlagsNew(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { l := newArgs(u.i, u.aa) - assert.Equal(t, len(u.ll), len(l)) assert.Equal(t, u.ll, l) }) } diff --git a/internal/view/cmd/helpers.go b/internal/view/cmd/helpers.go index 0068dcbe..13637f91 100644 --- a/internal/view/cmd/helpers.go +++ b/internal/view/cmd/helpers.go @@ -43,17 +43,7 @@ func SuggestSubCommand(command string, namespaces client.NamespaceNames, context p := NewInterpreter(command) var suggests []string switch { - case p.IsCowCmd(): - fallthrough - case p.IsHelpCmd(): - fallthrough - case p.IsAliasCmd(): - fallthrough - case p.IsBailCmd(): - fallthrough - case p.IsDirCmd(): - fallthrough - case p.IsAliasCmd(): + case p.IsCowCmd(), p.IsHelpCmd(), p.IsAliasCmd(), p.IsBailCmd(), p.IsDirCmd(): return nil case p.IsXrayCmd(): @@ -109,11 +99,11 @@ func completeNS(s string, nn client.NamespaceNames) []string { return suggests } -func completeCtx(s string, cc []string) []string { +func completeCtx(s string, contexts []string) []string { var suggests []string - for _, ctxName := range cc { + for _, ctxName := range contexts { if suggest, ok := ShouldAddSuggest(s, ctxName); ok { - if len(s) == 0 { + if s == "" { suggests = append(suggests, " "+suggest) continue } diff --git a/internal/view/cmd/interpreter.go b/internal/view/cmd/interpreter.go index fade256e..beaf8c1f 100644 --- a/internal/view/cmd/interpreter.go +++ b/internal/view/cmd/interpreter.go @@ -86,47 +86,37 @@ func (c *Interpreter) IsCowCmd() bool { // IsHelpCmd returns true if help cmd is detected. func (c *Interpreter) IsHelpCmd() bool { - _, ok := helpCmd[c.cmd] - return ok + return helpCmd.Has(c.cmd) } // IsBailCmd returns true if quit cmd is detected. func (c *Interpreter) IsBailCmd() bool { - _, ok := bailCmd[c.cmd] - return ok + return bailCmd.Has(c.cmd) } // IsAliasCmd returns true if alias cmd is detected. func (c *Interpreter) IsAliasCmd() bool { - _, ok := aliasCmd[c.cmd] - return ok + return aliasCmd.Has(c.cmd) } // IsXrayCmd returns true if xray cmd is detected. func (c *Interpreter) IsXrayCmd() bool { - _, ok := xrayCmd[c.cmd] - - return ok + return xrayCmd.Has(c.cmd) } // IsContextCmd returns true if context cmd is detected. func (c *Interpreter) IsContextCmd() bool { - _, ok := contextCmd[c.cmd] - - return ok + return contextCmd.Has(c.cmd) } // IsNamespaceCmd returns true if ns cmd is detected. func (c *Interpreter) IsNamespaceCmd() bool { - _, ok := namespaceCmd[c.cmd] - - return ok + return namespaceCmd.Has(c.cmd) } // IsDirCmd returns true if dir cmd is detected. func (c *Interpreter) IsDirCmd() bool { - _, ok := dirCmd[c.cmd] - return ok + return dirCmd.Has(c.cmd) } // IsRBACCmd returns true if rbac cmd is detected. @@ -169,37 +159,40 @@ func (c *Interpreter) CowArg() (string, bool) { } // RBACArgs returns the subject and topic is any. -func (c *Interpreter) RBACArgs() (string, string, bool) { +func (c *Interpreter) RBACArgs() (subject, verb string, ok bool) { if !c.IsRBACCmd() { - return "", "", false + return } tt := rbacRX.FindStringSubmatch(c.line) if len(tt) < 3 { - return "", "", false + return } + subject, verb, ok = tt[1], tt[2], true - return tt[1], tt[2], true + return } // XRayArgs return the gvr and ns if any. -func (c *Interpreter) XrayArgs() (string, string, bool) { +func (c *Interpreter) XrayArgs() (cmd, namespace string, ok bool) { if !c.IsXrayCmd() { - return "", "", false + return } gvr, ok1 := c.args[topicKey] if !ok1 { - return "", "", false + return } ns, ok2 := c.args[nsKey] switch { case ok1 && ok2: - return gvr, ns, true + cmd, namespace, ok = gvr, ns, true case ok1 && !ok2: - return gvr, "", true + cmd, namespace, ok = gvr, "", true default: - return "", "", false + return } + + return } // FilterArg returns the current filter if any. diff --git a/internal/view/cmd/types.go b/internal/view/cmd/types.go index 740a59c7..522f6c68 100644 --- a/internal/view/cmd/types.go +++ b/internal/view/cmd/types.go @@ -3,7 +3,11 @@ package cmd -import "regexp" +import ( + "regexp" + + "k8s.io/apimachinery/pkg/util/sets" +) const ( cowCmd = "cow" @@ -16,44 +20,45 @@ const ( ) var ( - rbacRX = regexp.MustCompile(`^can\s+([u|g|s]):\s*([\w-:]+)\s*$`) + rbacRX = regexp.MustCompile(`^can\s+([ugs]):\s*([\w-:]+)\s*$`) - contextCmd = map[string]struct{}{ - "ctx": {}, - "context": {}, - "contexts": {}, - } - namespaceCmd = map[string]struct{}{ - "ns": {}, - "namespace": {}, - "namespaces": {}, - } - dirCmd = map[string]struct{}{ - "dir": {}, - "d": {}, - "ls": {}, - } - bailCmd = map[string]struct{}{ - "q": {}, - "q!": {}, - "qa": {}, - "Q": {}, - "quit": {}, - "exit": {}, - } - helpCmd = map[string]struct{}{ - "?": {}, - "h": {}, - "help": {}, - } - aliasCmd = map[string]struct{}{ - "a": {}, - "alias": {}, - "aliases": {}, - } - xrayCmd = map[string]struct{}{ - "x": {}, - "xr": {}, - "xray": {}, - } + contextCmd = sets.New( + "ctx", + "context", + "contexts", + ) + namespaceCmd = sets.New( + "ns", + "namespace", + "namespaces", + ) + dirCmd = sets.New( + "dir", + "dirs", + "d", + "ls", + ) + bailCmd = sets.New( + "q", + "q!", + "qa", + "Q", + "quit", + "exit", + ) + helpCmd = sets.New( + "?", + "h", + "help", + ) + aliasCmd = sets.New( + "a", + "alias", + "aliases", + ) + xrayCmd = sets.New( + "x", + "xr", + "xray", + ) ) diff --git a/internal/view/command.go b/internal/view/command.go index be074f3e..4d44c842 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -17,6 +17,12 @@ import ( "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/slogs" "github.com/derailed/k9s/internal/view/cmd" + "k8s.io/apimachinery/pkg/util/sets" +) + +const ( + podCmd = "pod" + ctxCmd = "ctx" ) var ( @@ -39,8 +45,8 @@ func NewCommand(app *App) *Command { } // AliasesFor gather all known aliases for a given resource. -func (c *Command) AliasesFor(s string) []string { - return c.alias.AliasesFor(s) +func (c *Command) AliasesFor(gvr *client.GVR) sets.Set[string] { + return c.alias.AliasesFor(gvr) } // Init initializes the command. @@ -56,11 +62,11 @@ func (c *Command) Init(path string) error { } // Reset resets Command and reload aliases. -func (c *Command) Reset(path string, clear bool) error { +func (c *Command) Reset(path string, nuke bool) error { c.mx.Lock() defer c.mx.Unlock() - if clear { + if nuke { c.alias.Clear() } if _, err := c.alias.Ensure(path); err != nil { @@ -70,18 +76,17 @@ func (c *Command) Reset(path string, clear bool) error { return nil } -func allowedXRay(gvr client.GVR) bool { - gg := map[string]struct{}{ - "v1/pods": {}, - "v1/services": {}, - "apps/v1/deployments": {}, - "apps/v1/daemonsets": {}, - "apps/v1/statefulsets": {}, - "apps/v1/replicasets": {}, - } - _, ok := gg[gvr.String()] +var allowedCmds = sets.New[*client.GVR]( + client.PodGVR, + client.SvcGVR, + client.DpGVR, + client.DsGVR, + client.StsGVR, + client.RsGVR, +) - return ok +func allowedXRay(gvr *client.GVR) bool { + return allowedCmds.Has(gvr) } func (c *Command) contextCmd(p *cmd.Interpreter, pushCmd bool) error { @@ -102,7 +107,7 @@ func (c *Command) contextCmd(p *cmd.Interpreter, pushCmd bool) error { return c.exec(p, gvr, c.componentFor(gvr, ct, v), true, pushCmd) } -func (c *Command) namespaceCmd(p *cmd.Interpreter) bool { +func (*Command) namespaceCmd(p *cmd.Interpreter) bool { ns, ok := p.NSArg() if !ok { return false @@ -118,11 +123,10 @@ func (c *Command) namespaceCmd(p *cmd.Interpreter) bool { func (c *Command) aliasCmd(p *cmd.Interpreter, pushCmd bool) error { filter, _ := p.FilterArg() - gvr := client.NewGVR("aliases") - v := NewAlias(gvr) + v := NewAlias(client.AliGVR) v.SetFilter(filter) - return c.exec(p, gvr, v, false, pushCmd) + return c.exec(p, client.AliGVR, v, false, pushCmd) } func (c *Command) xrayCmd(p *cmd.Interpreter, pushCmd bool) error { @@ -152,7 +156,7 @@ func (c *Command) xrayCmd(p *cmd.Interpreter, pushCmd bool) error { } // Run execs the command by showing associated display. -func (c *Command) run(p *cmd.Interpreter, fqn string, clearStack bool, pushCmd bool) error { +func (c *Command) run(p *cmd.Interpreter, fqn string, clearStack, pushCmd bool) error { if c.specialCmd(p, pushCmd) { return nil } @@ -169,7 +173,7 @@ func (c *Command) run(p *cmd.Interpreter, fqn string, clearStack bool, pushCmd b slog.Debug("Successfully saved config", slogs.Context, context) } } - res, err := dao.AccessorFor(c.app.factory, client.NewGVR("contexts")) + res, err := dao.AccessorFor(c.app.factory, client.CtGVR) if err != nil { return err } @@ -215,9 +219,9 @@ func (c *Command) defaultCmd(isRoot bool) error { return c.run(cmd.NewInterpreter("context"), "", true, true) } - defCmd := "pod" + defCmd := podCmd if isRoot { - defCmd = "ctx" + defCmd = ctxCmd } p := cmd.NewInterpreter(c.app.Config.ActiveView()) if p.IsBlank() { @@ -281,22 +285,21 @@ func (c *Command) specialCmd(p *cmd.Interpreter, pushCmd bool) bool { return true } -func (c *Command) viewMetaFor(p *cmd.Interpreter) (client.GVR, *MetaViewer, error) { - agvr, exp, ok := c.alias.AsGVR(p.Cmd()) +func (c *Command) viewMetaFor(p *cmd.Interpreter) (*client.GVR, *MetaViewer, error) { + gvr, exp, ok := c.alias.AsGVR(p.Cmd()) if !ok { return client.NoGVR, nil, fmt.Errorf("`%s` command not found", p.Cmd()) } - gvr := agvr if exp != "" { ff := strings.Fields(exp) - ff[0] = agvr.String() + ff[0] = gvr.String() ap := cmd.NewInterpreter(strings.Join(ff, " ")) gvr = client.NewGVR(ap.Cmd()) p.Amend(ap) } v := MetaViewer{ - viewerFn: func(gvr client.GVR) ResourceViewer { + viewerFn: func(gvr *client.GVR) ResourceViewer { return NewScaleExtender(NewOwnerExtender(NewBrowser(gvr))) }, } @@ -307,7 +310,7 @@ func (c *Command) viewMetaFor(p *cmd.Interpreter) (client.GVR, *MetaViewer, erro return gvr, &v, nil } -func (c *Command) componentFor(gvr client.GVR, fqn string, v *MetaViewer) ResourceViewer { +func (*Command) componentFor(gvr *client.GVR, fqn string, v *MetaViewer) ResourceViewer { var view ResourceViewer if v.viewerFn != nil { view = v.viewerFn(gvr) @@ -323,7 +326,7 @@ func (c *Command) componentFor(gvr client.GVR, fqn string, v *MetaViewer) Resour return view } -func (c *Command) exec(p *cmd.Interpreter, gvr client.GVR, comp model.Component, clearStack bool, pushCmd bool) (err error) { +func (c *Command) exec(p *cmd.Interpreter, gvr *client.GVR, comp model.Component, clearStack, pushCmd bool) (err error) { defer func() { if e := recover(); e != nil { slog.Error("Failure detected during command exec", slogs.Error, e) @@ -331,13 +334,13 @@ func (c *Command) exec(p *cmd.Interpreter, gvr client.GVR, comp model.Component, slog.Debug("Dumping history buffer", slogs.CmdHist, c.app.cmdHistory.List()) slog.Error("Dumping stack", slogs.Stack, string(debug.Stack())) - p := cmd.NewInterpreter("pod") + ci := cmd.NewInterpreter(podCmd) cmds := c.app.cmdHistory.List() currentCommand := cmds[c.app.cmdHistory.CurrentIndex()] - if currentCommand != "pod" { - p = p.Reset(currentCommand) + if currentCommand != podCmd { + ci = ci.Reset(currentCommand) } - err = c.run(p, "", true, true) + err = c.run(ci, "", true, true) } }() diff --git a/internal/view/container.go b/internal/view/container.go index f6b7ad0e..26135f8e 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -27,7 +27,7 @@ type Container struct { } // NewContainer returns a new container view. -func NewContainer(gvr client.GVR) ResourceViewer { +func NewContainer(gvr *client.GVR) ResourceViewer { c := Container{} c.ResourceViewer = NewLogsExtender(NewBrowser(gvr), c.logOptions) c.SetEnvFn(c.k9sEnv) @@ -59,7 +59,7 @@ func (c *Container) decorateRows(data *model1.TableData) { } // Name returns the component name. -func (c *Container) Name() string { return containerTitle } +func (*Container) Name() string { return containerTitle } func (c *Container) bindDangerousKeys(aa *ui.KeyActions) { aa.Bulk(ui.KeyMap{ @@ -115,7 +115,7 @@ func (c *Container) logOptions(prev bool) (*dao.LogOptions, error) { opts := dao.LogOptions{ Path: c.GetTable().Path, Container: path, - Lines: int64(cfg.TailCount), + Lines: cfg.TailCount, SinceSeconds: cfg.SinceSeconds, SingleContainer: true, ShowTimestamp: cfg.ShowTime, @@ -125,7 +125,7 @@ func (c *Container) logOptions(prev bool) (*dao.LogOptions, error) { return &opts, nil } -func (c *Container) viewLogs(app *App, model ui.Tabular, gvr client.GVR, path string) { +func (c *Container) viewLogs(*App, ui.Tabular, *client.GVR, string) { c.ResourceViewer.(*LogsExtender).showLogs(c.GetTable().Path, false) } @@ -141,7 +141,7 @@ func (c *Container) showPFCmd(evt *tcell.EventKey) *tcell.EventKey { c.App().Flash().Errf("no port-forward defined") return nil } - pf := NewPortForward(client.NewGVR("portforwards")) + pf := NewPortForward(client.PfGVR) pf.SetContextFn(c.portForwardContext) if err := c.App().inject(pf, false); err != nil { c.App().Flash().Err(err) diff --git a/internal/view/container_test.go b/internal/view/container_test.go index 84787f6d..39c3abab 100644 --- a/internal/view/container_test.go +++ b/internal/view/container_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestContainerNew(t *testing.T) { - c := view.NewContainer(client.NewGVR("containers")) + c := view.NewContainer(client.CoGVR) - assert.Nil(t, c.Init(makeCtx())) + require.NoError(t, c.Init(makeCtx())) assert.Equal(t, "Containers", c.Name()) - assert.Equal(t, 19, len(c.Hints())) + assert.Len(t, c.Hints(), 19) } diff --git a/internal/view/context.go b/internal/view/context.go index 6b8f9864..c37f88d6 100644 --- a/internal/view/context.go +++ b/internal/view/context.go @@ -28,7 +28,7 @@ type Context struct { } // NewContext returns a new viewer. -func NewContext(gvr client.GVR) ResourceViewer { +func NewContext(gvr *client.GVR) ResourceViewer { c := Context{ ResourceViewer: NewBrowser(gvr), } @@ -96,14 +96,14 @@ func (c *Context) showRenameModal(name string, ok func(form *tview.Form, context app.Content.AddPage(renamePage, m, false, false) app.Content.ShowPage(renamePage) - for i := 0; i < f.GetButtonCount(); i++ { + for i := range f.GetButtonCount() { f.GetButton(i). SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()). SetLabelColorActivated(styles.ButtonFocusFgColor.Color()) } } -func (c *Context) useCtx(app *App, model ui.Tabular, gvr client.GVR, path string) { +func (c *Context) useCtx(app *App, _ ui.Tabular, gvr *client.GVR, path string) { slog.Debug("Using context", slogs.GVR, gvr, slogs.FQN, path, @@ -120,7 +120,7 @@ func useContext(app *App, name string) error { if app.Content.Top() != nil { app.Content.Top().Stop() } - res, err := dao.AccessorFor(app.factory, client.NewGVR("contexts")) + res, err := dao.AccessorFor(app.factory, client.CtGVR) if err != nil { return err } diff --git a/internal/view/context_test.go b/internal/view/context_test.go index a265459a..42fe5e60 100644 --- a/internal/view/context_test.go +++ b/internal/view/context_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestContext(t *testing.T) { - ctx := view.NewContext(client.NewGVR("contexts")) + ctx := view.NewContext(client.CtGVR) - assert.Nil(t, ctx.Init(makeCtx())) + require.NoError(t, ctx.Init(makeCtx())) assert.Equal(t, "Contexts", ctx.Name()) - assert.Equal(t, 5, len(ctx.Hints())) + assert.Len(t, ctx.Hints(), 5) } diff --git a/internal/view/cow.go b/internal/view/cow.go index 75b71b11..b65a7a7a 100644 --- a/internal/view/cow.go +++ b/internal/view/cow.go @@ -63,7 +63,7 @@ func (*Cow) InCmdMode() bool { func (c *Cow) talk() { says := c.says - if len(says) == 0 { + if says == "" { says = "Nothing to report here. Please move along..." } x, _, w, _ := c.GetRect() @@ -73,9 +73,11 @@ func (c *Cow) talk() { func cowTalk(says string, w int) string { msg := fmt.Sprintf("[red::]< [::b]Ruroh? %s[::-] >", says) buff := make([]string, 0, len(cow)+3) - buff = append(buff, "[red::] "+strings.Repeat("โ”€", len(says)+8)) - buff = append(buff, strings.TrimSuffix(msg, "\n")) - buff = append(buff, " "+strings.Repeat("โ”€", len(says)+8)) + buff = append(buff, + "[red::] "+strings.Repeat("โ”€", len(says)+8), + strings.TrimSuffix(msg, "\n"), + " "+strings.Repeat("โ”€", len(says)+8), + ) rCount := w/2 - 8 if rCount < 0 { rCount = w / 2 @@ -101,9 +103,9 @@ func (c *Cow) keyboard(evt *tcell.EventKey) *tcell.EventKey { // StylesChanged notifies the skin changes. func (c *Cow) StylesChanged(s *config.Styles) { - c.SetBackgroundColor(c.app.Styles.BgColor()) - c.SetTextColor(c.app.Styles.FgColor()) - c.SetBorderFocusColor(c.app.Styles.Frame().Border.FocusColor.Color()) + c.SetBackgroundColor(s.BgColor()) + c.SetTextColor(s.FgColor()) + c.SetBorderFocusColor(s.Frame().Border.FocusColor.Color()) } func (c *Cow) resetCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -116,10 +118,10 @@ func (c *Cow) Actions() *ui.KeyActions { } // Name returns the component name. -func (c *Cow) Name() string { return "cow" } +func (*Cow) Name() string { return "cow" } // Start starts the view updater. -func (c *Cow) Start() {} +func (*Cow) Start() {} // Stop terminates the updater. func (c *Cow) Stop() { @@ -132,7 +134,7 @@ func (c *Cow) Hints() model.MenuHints { } // ExtraHints returns additional hints. -func (c *Cow) ExtraHints() map[string]string { +func (*Cow) ExtraHints() map[string]string { return nil } diff --git a/internal/view/crd.go b/internal/view/crd.go index b3a113aa..1f6c7b37 100644 --- a/internal/view/crd.go +++ b/internal/view/crd.go @@ -14,7 +14,7 @@ type CRD struct { } // NewCRD returns a new viewer. -func NewCRD(gvr client.GVR) ResourceViewer { +func NewCRD(gvr *client.GVR) ResourceViewer { s := CRD{ ResourceViewer: NewOwnerExtender(NewBrowser(gvr)), } @@ -32,7 +32,7 @@ func (s *CRD) bindKeys(aa *ui.KeyActions) { }) } -func (s *CRD) showCRD(app *App, _ ui.Tabular, _ client.GVR, path string) { +func (*CRD) showCRD(app *App, _ ui.Tabular, _ *client.GVR, path string) { _, crd := client.Namespaced(path) app.gotoResource(crd, "", false, true) } diff --git a/internal/view/cronjob.go b/internal/view/cronjob.go index 0862e041..9f80f09b 100644 --- a/internal/view/cronjob.go +++ b/internal/view/cronjob.go @@ -35,7 +35,7 @@ type CronJob struct { } // NewCronJob returns a new viewer. -func NewCronJob(gvr client.GVR) ResourceViewer { +func NewCronJob(gvr *client.GVR) ResourceViewer { c := CronJob{ResourceViewer: NewVulnerabilityExtender( NewOwnerExtender(NewBrowser(gvr)), )} @@ -45,9 +45,9 @@ func NewCronJob(gvr client.GVR) ResourceViewer { return &c } -func (c *CronJob) showJobs(app *App, _ ui.Tabular, gvr client.GVR, fqn string) { +func (*CronJob) showJobs(app *App, _ ui.Tabular, gvr *client.GVR, fqn string) { slog.Debug("Showing Jobs", slogs.GVR, gvr, slogs.FQN, fqn) - o, err := app.factory.Get(gvr.String(), fqn, true, labels.Everything()) + o, err := app.factory.Get(gvr, fqn, true, labels.Everything()) if err != nil { app.Flash().Err(err) return @@ -64,7 +64,7 @@ func (c *CronJob) showJobs(app *App, _ ui.Tabular, gvr client.GVR, fqn string) { if err := app.Config.SetActiveNamespace(ns); err != nil { slog.Error("Unable to set active namespace during show pods", slogs.Error, err) } - v := NewJob(client.NewGVR("batch/v1/jobs")) + v := NewJob(client.JobGVR) v.SetContextFn(jobCtx(fqn, string(cj.UID))) if err := app.inject(v, false); err != nil { app.Flash().Err(err) @@ -95,7 +95,8 @@ func (c *CronJob) triggerCmd(evt *tcell.EventKey) *tcell.EventKey { if len(fqns) > 1 { msg = fmt.Sprintf("Trigger %d CronJobs?", len(fqns)) } - dialog.ShowConfirm(c.App().Styles.Dialog(), c.App().Content.Pages, "Confirm Job Trigger", msg, func() { + d := c.App().Styles.Dialog() + dialog.ShowConfirm(&d, c.App().Content.Pages, "Confirm Job Trigger", msg, func() { res, err := dao.AccessorFor(c.App().factory, c.GVR()) if err != nil { c.App().Flash().Err(fmt.Errorf("no accessor for %q", c.GVR())) @@ -149,7 +150,8 @@ func (c *CronJob) showSuspendDialog(cell *tview.TableCell, sel string) { title = "Resume" } - dialog.ShowConfirm(c.App().Styles.Dialog(), c.App().Content.Pages, title, sel, func() { + d := c.App().Styles.Dialog() + dialog.ShowConfirm(&d, c.App().Content.Pages, title, sel, func() { ctx, cancel := context.WithTimeout(context.Background(), c.App().Conn().Config().CallTimeout()) defer cancel() diff --git a/internal/view/details.go b/internal/view/details.go index 18db2883..f785db74 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -59,9 +59,9 @@ func NewDetails(app *App, title, subject, contentType string, searchable bool) * return &d } -func (d *Details) SetCommand(*cmd.Interpreter) {} -func (d *Details) SetFilter(string) {} -func (d *Details) SetLabelFilter(map[string]string) {} +func (*Details) SetCommand(*cmd.Interpreter) {} +func (*Details) SetFilter(string) {} +func (*Details) SetLabelFilter(map[string]string) {} // Init initializes the viewer. func (d *Details) Init(_ context.Context) error { @@ -120,7 +120,7 @@ func (d *Details) TextFiltered(lines []string, matches fuzzy.Matches) { } // BufferChanged indicates the buffer was changed. -func (d *Details) BufferChanged(_, _ string) {} +func (*Details) BufferChanged(_, _ string) {} // BufferCompleted indicates input was accepted. func (d *Details) BufferCompleted(text, _ string) { @@ -161,9 +161,9 @@ func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey { // StylesChanged notifies the skin changed. func (d *Details) StylesChanged(s *config.Styles) { - d.SetBackgroundColor(d.app.Styles.BgColor()) - d.text.SetTextColor(d.app.Styles.FgColor()) - d.SetBorderFocusColor(d.app.Styles.Frame().Border.FocusColor.Color()) + d.SetBackgroundColor(s.BgColor()) + d.text.SetTextColor(s.FgColor()) + d.SetBorderFocusColor(s.Frame().Border.FocusColor.Color()) d.TextChanged(d.model.Peek()) } @@ -192,7 +192,7 @@ func (d *Details) Actions() *ui.KeyActions { func (d *Details) Name() string { return d.title } // Start starts the view updater. -func (d *Details) Start() {} +func (*Details) Start() {} // Stop terminates the updater. func (d *Details) Stop() { @@ -205,7 +205,7 @@ func (d *Details) Hints() model.MenuHints { } // ExtraHints returns additional hints. -func (d *Details) ExtraHints() map[string]string { +func (*Details) ExtraHints() map[string]string { return nil } @@ -262,7 +262,7 @@ func (d *Details) prevCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (d *Details) filterCmd(evt *tcell.EventKey) *tcell.EventKey { +func (d *Details) filterCmd(*tcell.EventKey) *tcell.EventKey { d.model.Filter(d.cmdBuff.GetText()) d.cmdBuff.SetActive(false) d.updateTitle() @@ -279,7 +279,7 @@ func (d *Details) activateCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (d *Details) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { +func (d *Details) eraseCmd(*tcell.EventKey) *tcell.EventKey { if !d.cmdBuff.IsActive() { return nil } @@ -304,7 +304,7 @@ func (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey { +func (d *Details) saveCmd(*tcell.EventKey) *tcell.EventKey { if path, err := saveYAML(d.app.Config.K9s.ContextScreenDumpDir(), d.title, d.text.GetText(true)); err != nil { d.app.Flash().Err(err) } else { @@ -320,9 +320,12 @@ func (d *Details) updateTitle() { } fmat := fmt.Sprintf(detailsTitleFmt, d.title, d.subject) - buff := d.cmdBuff.GetText() + var ( + buff = d.cmdBuff.GetText() + styles = d.app.Styles.Frame() + ) if buff == "" { - d.SetTitle(ui.SkinTitle(fmat, d.app.Styles.Frame())) + d.SetTitle(ui.SkinTitle(fmat, &styles)) return } @@ -330,5 +333,5 @@ func (d *Details) updateTitle() { buff += fmt.Sprintf("[%d:%d]", d.currentRegion+1, d.maxRegions) } fmat += fmt.Sprintf(ui.SearchFmt, buff) - d.SetTitle(ui.SkinTitle(fmat, d.app.Styles.Frame())) + d.SetTitle(ui.SkinTitle(fmat, &styles)) } diff --git a/internal/view/dir.go b/internal/view/dir.go index 825d79ec..58de96ec 100644 --- a/internal/view/dir.go +++ b/internal/view/dir.go @@ -36,7 +36,7 @@ type Dir struct { // NewDir returns a new instance. func NewDir(path string) ResourceViewer { d := Dir{ - ResourceViewer: NewBrowser(client.NewGVR("dir")), + ResourceViewer: NewBrowser(client.DirGVR), path: path, } d.GetTable().SetBorderFocusColor(tcell.ColorAliceBlue) @@ -132,7 +132,7 @@ func (d *Dir) editCmd(evt *tcell.EventKey) *tcell.EventKey { d.Stop() defer d.Start() - if !edit(d.App(), shellOpts{clear: true, args: []string{sel}}) { + if !edit(d.App(), &shellOpts{clear: true, args: []string{sel}}) { d.App().Flash().Errf("Failed to launch editor") } @@ -219,7 +219,7 @@ func (d *Dir) applyCmd(evt *tcell.EventKey) *tcell.EventKey { args = append(args, "apply") args = append(args, opts...) args = append(args, sel) - res, err := runKu(d.App(), shellOpts{clear: false, args: args}) + res, err := runKu(d.App(), &shellOpts{clear: false, args: args}) if err != nil { res = "status:\n " + err.Error() + "\nmessage:\n" + fmtResults(res) } else { @@ -254,12 +254,13 @@ func (d *Dir) delCmd(evt *tcell.EventKey) *tcell.EventKey { d.Stop() defer d.Start() msg := fmt.Sprintf("Delete resource(s) in %s %s", msgResource, sel) - dialog.ShowConfirm(d.App().Styles.Dialog(), d.App().Content.Pages, "Confirm Delete", msg, func() { + dlg := d.App().Styles.Dialog() + dialog.ShowConfirm(&dlg, d.App().Content.Pages, "Confirm Delete", msg, func() { args := make([]string, 0, 10) args = append(args, "delete") args = append(args, opts...) args = append(args, sel) - res, err := runKu(d.App(), shellOpts{clear: false, args: args}) + res, err := runKu(d.App(), &shellOpts{clear: false, args: args}) if err != nil { res = "status:\n " + err.Error() + "\nmessage:\n" + fmtResults(res) } else { diff --git a/internal/view/dir_test.go b/internal/view/dir_test.go index 7757eb8a..f00dc355 100644 --- a/internal/view/dir_test.go +++ b/internal/view/dir_test.go @@ -8,12 +8,13 @@ import ( "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDir(t *testing.T) { v := view.NewDir("/fred") - assert.Nil(t, v.Init(makeCtx())) + require.NoError(t, v.Init(makeCtx())) assert.Equal(t, "Directory", v.Name()) - assert.Equal(t, 7, len(v.Hints())) + assert.Len(t, v.Hints(), 7) } diff --git a/internal/view/dp.go b/internal/view/dp.go index 3c6760c8..02f80076 100644 --- a/internal/view/dp.go +++ b/internal/view/dp.go @@ -21,7 +21,7 @@ type Deploy struct { } // NewDeploy returns a new deployment view. -func NewDeploy(gvr client.GVR) ResourceViewer { +func NewDeploy(gvr *client.GVR) ResourceViewer { var d Deploy d.ResourceViewer = NewPortForwardExtender( NewVulnerabilityExtender( @@ -60,10 +60,10 @@ func (d *Deploy) logOptions(prev bool) (*dao.LogOptions, error) { return nil, err } - return podLogOptions(d.App(), path, prev, dp.ObjectMeta, dp.Spec.Template.Spec), nil + return podLogOptions(d.App(), path, prev, &dp.ObjectMeta, &dp.Spec.Template.Spec), nil } -func (d *Deploy) showPods(app *App, model ui.Tabular, gvr client.GVR, fqn string) { +func (d *Deploy) showPods(app *App, _ ui.Tabular, _ *client.GVR, fqn string) { dp, err := d.getInstance(fqn) if err != nil { app.Flash().Err(err) diff --git a/internal/view/dp_test.go b/internal/view/dp_test.go index a3e8a51f..270646d1 100644 --- a/internal/view/dp_test.go +++ b/internal/view/dp_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDeploy(t *testing.T) { - v := view.NewDeploy(client.NewGVR("apps/v1/deployments")) + v := view.NewDeploy(client.DpGVR) - assert.Nil(t, v.Init(makeCtx())) + require.NoError(t, v.Init(makeCtx())) assert.Equal(t, "Deployments", v.Name()) - assert.Equal(t, 16, len(v.Hints())) + assert.Len(t, v.Hints(), 16) } diff --git a/internal/view/drain_dialog.go b/internal/view/drain_dialog.go index 544c9eb9..103b575a 100644 --- a/internal/view/drain_dialog.go +++ b/internal/view/drain_dialog.go @@ -80,7 +80,7 @@ func ShowDrain(view ResourceViewer, sels []string, opts dao.DrainOptions, okFn D } path += "?" modal.SetText(path) - modal.SetDoneFunc(func(_ int, b string) { + modal.SetDoneFunc(func(int, string) { DismissDrain(view, pages) }) diff --git a/internal/view/ds.go b/internal/view/ds.go index 08aad213..bbe93484 100644 --- a/internal/view/ds.go +++ b/internal/view/ds.go @@ -18,7 +18,7 @@ type DaemonSet struct { } // NewDaemonSet returns a new viewer. -func NewDaemonSet(gvr client.GVR) ResourceViewer { +func NewDaemonSet(gvr *client.GVR) ResourceViewer { var d DaemonSet d.ResourceViewer = NewPortForwardExtender( NewVulnerabilityExtender( @@ -47,7 +47,7 @@ func (d *DaemonSet) bindKeys(aa *ui.KeyActions) { }) } -func (d *DaemonSet) showPods(app *App, model ui.Tabular, _ client.GVR, path string) { +func (d *DaemonSet) showPods(app *App, _ ui.Tabular, _ *client.GVR, path string) { var res dao.DaemonSet res.Init(app.factory, d.GVR()) @@ -70,12 +70,12 @@ func (d *DaemonSet) logOptions(prev bool) (*dao.LogOptions, error) { return nil, err } - return podLogOptions(d.App(), path, prev, ds.ObjectMeta, ds.Spec.Template.Spec), nil + return podLogOptions(d.App(), path, prev, &ds.ObjectMeta, &ds.Spec.Template.Spec), nil } func (d *DaemonSet) getInstance(fqn string) (*appsv1.DaemonSet, error) { var ds dao.DaemonSet - ds.Init(d.App().factory, client.NewGVR("apps/v1/daemonsets")) + ds.Init(d.App().factory, client.DsGVR) return ds.GetInstance(fqn) } diff --git a/internal/view/ds_test.go b/internal/view/ds_test.go index 94545218..62e8da20 100644 --- a/internal/view/ds_test.go +++ b/internal/view/ds_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDaemonSet(t *testing.T) { - v := view.NewDaemonSet(client.NewGVR("apps/v1/daemonsets")) + v := view.NewDaemonSet(client.DsGVR) - assert.Nil(t, v.Init(makeCtx())) + require.NoError(t, v.Init(makeCtx())) assert.Equal(t, "DaemonSets", v.Name()) - assert.Equal(t, 17, len(v.Hints())) + assert.Len(t, v.Hints(), 17) } diff --git a/internal/view/event.go b/internal/view/event.go index b75c975a..8e4cf897 100644 --- a/internal/view/event.go +++ b/internal/view/event.go @@ -15,7 +15,7 @@ type Event struct { } // NewEvent returns a new alias view. -func NewEvent(gvr client.GVR) ResourceViewer { +func NewEvent(gvr *client.GVR) ResourceViewer { e := Event{ ResourceViewer: NewBrowser(gvr), } diff --git a/internal/view/exec.go b/internal/view/exec.go index 5a9a6abb..35b041b2 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -53,7 +53,7 @@ func (s shellOpts) String() string { return fmt.Sprintf("%s %s", s.binary, strings.Join(s.args, " ")) } -func runK(a *App, opts shellOpts) error { +func runK(a *App, opts *shellOpts) error { bin, err := exec.LookPath("kubectl") if errors.Is(err, exec.ErrDot) { return fmt.Errorf("kubectl command must not be in the current working directory: %w", err) @@ -95,7 +95,7 @@ func runK(a *App, opts shellOpts) error { return errs } -func run(a *App, opts shellOpts) (bool, chan error, chan string) { +func run(a *App, opts *shellOpts) (ok bool, errC chan error, outC chan string) { errChan := make(chan error, 1) statusChan := make(chan string, 1) @@ -120,7 +120,7 @@ func run(a *App, opts shellOpts) (bool, chan error, chan string) { }), errChan, statusChan } -func edit(a *App, opts shellOpts) bool { +func edit(a *App, opts *shellOpts) bool { var ( bin string err error @@ -168,7 +168,7 @@ func edit(a *App, opts shellOpts) bool { return status } -func execute(opts shellOpts, statusChan chan<- string) error { +func execute(opts *shellOpts, statusChan chan<- string) error { if opts.clear { clearScreen() } @@ -235,7 +235,7 @@ func execute(opts shellOpts, statusChan chan<- string) error { return nil } -func runKu(a *App, opts shellOpts) (string, error) { +func runKu(a *App, opts *shellOpts) (string, error) { bin, err := exec.LookPath("kubectl") if errors.Is(err, exec.ErrDot) { slog.Error("Kubectl exec can not reside in current working directory", slogs.Error, err) @@ -264,7 +264,7 @@ func runKu(a *App, opts shellOpts) (string, error) { return oneShoot(opts) } -func oneShoot(opts shellOpts) (string, error) { +func oneShoot(opts *shellOpts) (string, error) { if opts.clear { clearScreen() } @@ -301,7 +301,8 @@ func launchNodeShell(v model.Igniter, a *App, node string) { } msg := fmt.Sprintf("Launching node shell on %s...", node) - dialog.ShowPrompt(a.Styles.Dialog(), a.Content.Pages, "Launching", msg, func(ctx context.Context) { + d := a.Styles.Dialog() + dialog.ShowPrompt(&d, a.Content.Pages, "Launching", msg, func(ctx context.Context) { err := launchShellPod(ctx, a, node) if err != nil { if !errors.Is(err, context.Canceled) { @@ -357,7 +358,11 @@ func sshIn(a *App, fqn, co string) error { slog.Debug("Running command with args", slogs.Args, args) c := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold) - err = runK(a, shellOpts{clear: true, banner: c.Sprintf(bannerFmt, fqn, co), args: args}) + err = runK(a, &shellOpts{ + clear: true, + banner: c.Sprintf(bannerFmt, fqn, co), + args: args}, + ) if err != nil { return fmt.Errorf("shell exec failed: %w", err) } @@ -407,8 +412,8 @@ func launchShellPod(ctx context.Context, a *App, node string) error { return err } - for i := 0; i < k9sShellRetryCount; i++ { - o, err := a.factory.Get("v1/pods", client.FQN(spo.Namespace, k9sShellPodName()), true, labels.Everything()) + for i := range k9sShellRetryCount { + o, err := a.factory.Get(client.PodGVR, client.FQN(spo.Namespace, k9sShellPodName()), true, labels.Everything()) if err != nil { select { case <-ctx.Done(): @@ -444,7 +449,7 @@ func k9sShellPodName() string { return fmt.Sprintf("%s-%d", k9sShell, os.Getpid()) } -func k9sShellPod(node string, cfg config.ShellPod) *v1.Pod { +func k9sShellPod(node string, cfg *config.ShellPod) *v1.Pod { var grace int64 var priv = true @@ -516,7 +521,7 @@ func asResource(r config.Limits) v1.ResourceRequirements { } } -func pipe(_ context.Context, opts shellOpts, statusChan chan<- string, w, e *bytes.Buffer, cmds ...*exec.Cmd) error { +func pipe(_ context.Context, opts *shellOpts, statusChan chan<- string, w, e *bytes.Buffer, cmds ...*exec.Cmd) error { if len(cmds) == 0 { return nil } @@ -556,7 +561,7 @@ func pipe(_ context.Context, opts shellOpts, statusChan chan<- string, w, e *byt } last := len(cmds) - 1 - for i := 0; i < len(cmds); i++ { + for i := range cmds { cmds[i].Stderr = os.Stderr if i+1 < len(cmds) { r, w := io.Pipe() diff --git a/internal/view/group.go b/internal/view/group.go index 0cfe42dd..eb5026cc 100644 --- a/internal/view/group.go +++ b/internal/view/group.go @@ -18,7 +18,7 @@ type Group struct { } // NewGroup returns a new subject viewer. -func NewGroup(gvr client.GVR) ResourceViewer { +func NewGroup(gvr *client.GVR) ResourceViewer { g := Group{ResourceViewer: NewBrowser(gvr)} g.AddBindKeysFn(g.bindKeys) g.SetContextFn(g.subjectCtx) @@ -34,7 +34,7 @@ func (g *Group) bindKeys(aa *ui.KeyActions) { }) } -func (g *Group) subjectCtx(ctx context.Context) context.Context { +func (*Group) subjectCtx(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeySubjectKind, "Group") } diff --git a/internal/view/helm_chart.go b/internal/view/helm_chart.go index c3d595ba..f2537b01 100644 --- a/internal/view/helm_chart.go +++ b/internal/view/helm_chart.go @@ -18,7 +18,7 @@ type HelmChart struct { } // NewHelmChart returns a new helm-chart view. -func NewHelmChart(gvr client.GVR) ResourceViewer { +func NewHelmChart(gvr *client.GVR) ResourceViewer { c := HelmChart{ ResourceViewer: NewValueExtender(NewBrowser(gvr)), } @@ -33,7 +33,7 @@ func NewHelmChart(gvr client.GVR) ResourceViewer { return &c } -func (c *HelmChart) chartContext(ctx context.Context) context.Context { +func (*HelmChart) chartContext(ctx context.Context) context.Context { return ctx } @@ -45,8 +45,8 @@ func (c *HelmChart) bindKeys(aa *ui.KeyActions) { }) } -func (c *HelmChart) viewReleases(app *App, model ui.Tabular, _ client.GVR, path string) { - v := NewHistory(client.NewGVR("helm-history")) +func (c *HelmChart) viewReleases(app *App, _ ui.Tabular, _ *client.GVR, _ string) { + v := NewHistory(client.HmhGVR) v.SetContextFn(c.helmContext) if err := app.inject(v, false); err != nil { app.Flash().Err(err) diff --git a/internal/view/helm_history.go b/internal/view/helm_history.go index 4bbd7c31..41b669a8 100644 --- a/internal/view/helm_history.go +++ b/internal/view/helm_history.go @@ -25,7 +25,7 @@ type History struct { } // NewHistory returns a new helm-history view. -func NewHistory(gvr client.GVR) ResourceViewer { +func NewHistory(gvr *client.GVR) ResourceViewer { h := History{ ResourceViewer: NewValueExtender(NewBrowser(gvr)), } @@ -49,7 +49,7 @@ func (h *History) Init(ctx context.Context) error { return nil } -func (h *History) HistoryContext(ctx context.Context) context.Context { +func (*History) HistoryContext(ctx context.Context) context.Context { return ctx } @@ -66,7 +66,7 @@ func (h *History) bindKeys(aa *ui.KeyActions) { }) } -func (h *History) getValsCmd(app *App, _ ui.Tabular, _ client.GVR, path string) { +func (h *History) getValsCmd(app *App, _ ui.Tabular, _ *client.GVR, path string) { ns, n := client.Namespaced(path) tt := strings.Split(n, ":") if len(tt) < 2 { diff --git a/internal/view/help.go b/internal/view/help.go index dd3609f1..e610c074 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -39,7 +39,7 @@ type Help struct { // NewHelp returns a new help viewer. func NewHelp(app *App) *Help { return &Help{ - Table: NewTable(client.NewGVR("help")), + Table: NewTable(client.HlpGVR), hints: app.Content.Top().Hints, } } @@ -154,7 +154,7 @@ func (h *Help) addExtras(extras map[string]string, col, size int) { } } -func (h *Help) showNav() model.MenuHints { +func (*Help) showNav() model.MenuHints { return model.MenuHints{ { Mnemonic: "g", @@ -224,7 +224,7 @@ func (h *Help) showHotKeys() (model.MenuHints, error) { return mm, nil } -func (h *Help) showGeneral() model.MenuHints { +func (*Help) showGeneral() model.MenuHints { return model.MenuHints{ { Mnemonic: "?", @@ -341,8 +341,8 @@ func (h *Help) updateStyle() { info = style.Foreground(h.app.Styles.K9s.Help.FgColor.Color()) heading = style.Foreground(h.app.Styles.K9s.Help.SectionColor.Color()) ) - for col := 0; col < h.GetColumnCount(); col++ { - for row := 0; row < h.GetRowCount(); row++ { + for col := range h.GetColumnCount() { + for row := range h.GetRowCount() { c := h.GetCell(row, col) if c == nil { continue @@ -384,7 +384,7 @@ func (h *Help) titleCell(title string) *tview.TableCell { return c } -func padCellWithRef(s string, width int, ref interface{}) *tview.TableCell { +func padCellWithRef(s string, width int, ref any) *tview.TableCell { return padCell(s, width).SetReference(ref) } diff --git a/internal/view/help_test.go b/internal/view/help_test.go index b6f19c78..1f850169 100644 --- a/internal/view/help_test.go +++ b/internal/view/help_test.go @@ -11,19 +11,20 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestHelp(t *testing.T) { ctx := makeCtx() app := ctx.Value(internal.KeyApp).(*view.App) - po := view.NewPod(client.NewGVR("v1/pods")) - assert.NoError(t, po.Init(ctx)) + po := view.NewPod(client.PodGVR) + require.NoError(t, po.Init(ctx)) app.Content.Push(po) v := view.NewHelp(app) - assert.Nil(t, v.Init(ctx)) + require.NoError(t, v.Init(ctx)) assert.Equal(t, 29, v.GetRowCount()) assert.Equal(t, 8, v.GetColumnCount()) assert.Equal(t, "", strings.TrimSpace(v.GetCell(1, 0).Text)) diff --git a/internal/view/helpers.go b/internal/view/helpers.go index 11df3c02..2e39d07b 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -28,24 +28,17 @@ import ( "github.com/derailed/tview" "github.com/sahilm/fuzzy" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" ) -func aliasesFor(m v1.APIResource, aa []string) map[string]struct{} { - rr := make(map[string]struct{}) - rr[m.Name] = struct{}{} - for _, a := range aa { - rr[a] = struct{}{} - } - if m.ShortNames != nil { - for _, a := range m.ShortNames { - rr[a] = struct{}{} - } - } +func aliases(m *v1.APIResource, aa sets.Set[string]) sets.Set[string] { + ss := sets.New(aa.UnsortedList()...) + ss.Insert(m.ShortNames...) if m.SingularName != "" { - rr[m.SingularName] = struct{}{} + ss.Insert(m.SingularName) } - return rr + return ss } func clipboardWrite(text string) error { @@ -74,10 +67,10 @@ func cpCmd(flash *model.Flash, v *tview.TextView) func(*tcell.EventKey) *tcell.E } } -func parsePFAnn(s string) (string, string, bool) { +func parsePFAnn(s string) (port, lport string, ok bool) { tokens := strings.Split(s, ":") if len(tokens) != 2 { - return "", "", false + return } return tokens[0], tokens[1], true @@ -134,7 +127,7 @@ func defaultEnv(c *client.Config, path string, header model1.Header, row *model1 return env } -func describeResource(app *App, m ui.Tabular, gvr client.GVR, path string) { +func describeResource(app *App, _ ui.Tabular, gvr *client.GVR, path string) { v := NewLiveView(app, "Describe", model.NewDescribe(gvr, path)) if err := app.inject(v, false); err != nil { app.Flash().Err(err) @@ -151,7 +144,7 @@ func toLabelsStr(labels map[string]string) string { } func showPods(app *App, path, labelSel, fieldSel string) { - v := NewPod(client.NewGVR("v1/pods")) + v := NewPod(client.PodGVR) v.SetContextFn(podCtx(app, path, fieldSel)) v.SetLabelFilter(cmd.ToLabels(labelSel)) @@ -209,7 +202,7 @@ func containerID(path, co string) string { } // UrlFor computes fq url for a given benchmark configuration. -func urlFor(cfg config.BenchConfig, port string) string { +func urlFor(cfg *config.BenchConfig, port string) string { host := "localhost" if cfg.HTTP.Host != "" { host = cfg.HTTP.Host @@ -234,12 +227,12 @@ func decorateCpuMemHeaderRows(app *App, data *model1.TableData) { for colIndex, header := range data.Header() { var check string if header.Name == "%CPU/L" { - check = "cpu" + check = config.CPU } if header.Name == "%MEM/L" { - check = "memory" + check = config.MEM } - if len(check) == 0 { + if check == "" { continue } data.RowsRange(func(_ int, re model1.RowEvent) bool { @@ -258,7 +251,7 @@ func decorateCpuMemHeaderRows(app *App, data *model1.TableData) { return true } color := app.Config.K9s.Thresholds.SeverityColor(check, n) - if len(color) > 0 { + if color != "" { re.Row.Fields[colIndex] = "[" + color + "::b]" + re.Row.Fields[colIndex] } diff --git a/internal/view/helpers_test.go b/internal/view/helpers_test.go index 87500f92..9f941178 100644 --- a/internal/view/helpers_test.go +++ b/internal/view/helpers_test.go @@ -133,7 +133,7 @@ func TestK8sEnv(t *testing.T) { c := client.NewConfig(&flags) env := k8sEnv(c) - assert.Equal(t, 5, len(env)) + assert.Len(t, env, 5) assert.Equal(t, cl, env["CLUSTER"]) assert.Equal(t, ctx, env["CONTEXT"]) assert.Equal(t, u, env["USER"]) @@ -160,7 +160,7 @@ func TestK9sEnv(t *testing.T) { } env := defaultEnv(c, "fred/blee", h, &r) - assert.Equal(t, 10, len(env)) + assert.Len(t, env, 10) assert.Equal(t, cl, env["CLUSTER"]) assert.Equal(t, ctx, env["CONTEXT"]) assert.Equal(t, u, env["USER"]) @@ -241,7 +241,7 @@ func TestUrlFor(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, urlFor(u.cfg, u.port)) + assert.Equal(t, u.e, urlFor(&u.cfg, u.port)) }) } } diff --git a/internal/view/image_extender.go b/internal/view/image_extender.go index c3675acc..eeb87920 100644 --- a/internal/view/image_extender.go +++ b/internal/view/image_extender.go @@ -64,7 +64,7 @@ func (s *ImageExtender) bindKeys(aa *ui.KeyActions) { aa.Add(ui.KeyI, ui.NewKeyAction("Set Image", s.setImageCmd, false)) } -func (s *ImageExtender) setImageCmd(evt *tcell.EventKey) *tcell.EventKey { +func (s *ImageExtender) setImageCmd(*tcell.EventKey) *tcell.EventKey { path := s.GetTable().GetSelectedItem() if path == "" { return nil @@ -102,10 +102,12 @@ func (s *ImageExtender) makeSetImageForm(fqn string) (*tview.Form, error) { } formContainerLines := make([]*imageFormSpec, 0, len(podSpec.InitContainers)+len(podSpec.Containers)) - for _, spec := range podSpec.InitContainers { + for i := range podSpec.InitContainers { + spec := podSpec.InitContainers[i] formContainerLines = append(formContainerLines, &imageFormSpec{init: true, name: spec.Name, dockerImage: spec.Image}) } - for _, spec := range podSpec.Containers { + for i := range podSpec.Containers { + spec := podSpec.Containers[i] formContainerLines = append(formContainerLines, &imageFormSpec{name: spec.Name, dockerImage: spec.Image}) } @@ -148,7 +150,7 @@ func (s *ImageExtender) makeSetImageForm(fqn string) (*tview.Form, error) { }) } - for i := 0; i < f.GetButtonCount(); i++ { + for i := range f.GetButtonCount() { f.GetButton(i). SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()). SetLabelColorActivated(styles.ButtonFocusFgColor.Color()) diff --git a/internal/view/img_scan.go b/internal/view/img_scan.go index 58b1002d..e548e590 100644 --- a/internal/view/img_scan.go +++ b/internal/view/img_scan.go @@ -28,7 +28,7 @@ type ImageScan struct { } // NewImageScan returns a new scans view. -func NewImageScan(gvr client.GVR) ResourceViewer { +func NewImageScan(gvr *client.GVR) ResourceViewer { v := ImageScan{} v.ResourceViewer = NewBrowser(gvr) v.AddBindKeysFn(v.bindKeys) @@ -39,20 +39,19 @@ func NewImageScan(gvr client.GVR) ResourceViewer { } // Name returns the component name. -func (s *ImageScan) Name() string { return imgScanTitle } +func (*ImageScan) Name() string { return imgScanTitle } -func (c *ImageScan) bindKeys(aa *ui.KeyActions) { +func (i *ImageScan) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlZ, tcell.KeyCtrlW) - aa.Bulk(ui.KeyMap{ - ui.KeyShiftL: ui.NewKeyAction("Sort Lib", c.GetTable().SortColCmd("LIBRARY", false), true), - ui.KeyShiftS: ui.NewKeyAction("Sort Severity", c.GetTable().SortColCmd("SEVERITY", false), true), - ui.KeyShiftF: ui.NewKeyAction("Sort Fixed-in", c.GetTable().SortColCmd("FIXED-IN", false), true), - ui.KeyShiftV: ui.NewKeyAction("Sort Vulnerability", c.GetTable().SortColCmd("VULNERABILITY", false), true), + ui.KeyShiftL: ui.NewKeyAction("Sort Lib", i.GetTable().SortColCmd("LIBRARY", false), true), + ui.KeyShiftS: ui.NewKeyAction("Sort Severity", i.GetTable().SortColCmd("SEVERITY", false), true), + ui.KeyShiftF: ui.NewKeyAction("Sort Fixed-in", i.GetTable().SortColCmd("FIXED-IN", false), true), + ui.KeyShiftV: ui.NewKeyAction("Sort Vulnerability", i.GetTable().SortColCmd("VULNERABILITY", false), true), }) } -func (s *ImageScan) viewCVE(app *App, _ ui.Tabular, _ client.GVR, path string) { +func (*ImageScan) viewCVE(app *App, _ ui.Tabular, _ *client.GVR, path string) { bin := browseLinux if runtime.GOOS == "darwin" { bin = browseOSX @@ -69,7 +68,7 @@ func (s *ImageScan) viewCVE(app *App, _ ui.Tabular, _ client.GVR, path string) { } site += cve - ok, errChan, _ := run(app, shellOpts{ + ok, errChan, _ := run(app, &shellOpts{ background: true, binary: bin, args: []string{site}, diff --git a/internal/view/job.go b/internal/view/job.go index c4562273..cccb65b0 100644 --- a/internal/view/job.go +++ b/internal/view/job.go @@ -21,7 +21,7 @@ type Job struct { } // NewJob returns a new viewer. -func NewJob(gvr client.GVR) ResourceViewer { +func NewJob(gvr *client.GVR) ResourceViewer { var j Job j.ResourceViewer = NewVulnerabilityExtender( @@ -35,8 +35,8 @@ func NewJob(gvr client.GVR) ResourceViewer { return &j } -func (*Job) showPods(app *App, model ui.Tabular, gvr client.GVR, path string) { - o, err := app.factory.Get(gvr.String(), path, true, labels.Everything()) +func (*Job) showPods(app *App, _ ui.Tabular, gvr *client.GVR, path string) { + o, err := app.factory.Get(gvr, path, true, labels.Everything()) if err != nil { app.Flash().Err(err) return @@ -62,12 +62,12 @@ func (j *Job) logOptions(prev bool) (*dao.LogOptions, error) { return nil, err } - return podLogOptions(j.App(), path, prev, job.ObjectMeta, job.Spec.Template.Spec), nil + return podLogOptions(j.App(), path, prev, &job.ObjectMeta, &job.Spec.Template.Spec), nil } func (j *Job) getInstance(fqn string) (*batchv1.Job, error) { var job dao.Job - job.Init(j.App().factory, client.NewGVR("batch/v1/jobs")) + job.Init(j.App().factory, client.JobGVR) return job.GetInstance(fqn) } diff --git a/internal/view/live_view.go b/internal/view/live_view.go index 69746178..dcbbd070 100644 --- a/internal/view/live_view.go +++ b/internal/view/live_view.go @@ -62,9 +62,9 @@ func NewLiveView(app *App, title string, m model.ResourceViewer) *LiveView { return &v } -func (v *LiveView) SetCommand(*cmd.Interpreter) {} -func (v *LiveView) SetFilter(string) {} -func (v *LiveView) SetLabelFilter(map[string]string) {} +func (*LiveView) SetCommand(*cmd.Interpreter) {} +func (*LiveView) SetFilter(string) {} +func (*LiveView) SetLabelFilter(map[string]string) {} // Init initializes the viewer. func (v *LiveView) Init(_ context.Context) error { @@ -129,7 +129,7 @@ func (v *LiveView) ResourceChanged(lines []string, matches fuzzy.Matches) { } // BufferChanged indicates the buffer was changed. -func (v *LiveView) BufferChanged(_, _ string) {} +func (*LiveView) BufferChanged(_, _ string) {} // BufferCompleted indicates input was accepted. func (v *LiveView) BufferCompleted(text, _ string) { @@ -193,7 +193,7 @@ func (v *LiveView) editCmd(evt *tcell.EventKey) *tcell.EventKey { } // ToggleRefreshCmd is used for pausing the refreshing of data on config map and secrets. -func (v *LiveView) toggleRefreshCmd(evt *tcell.EventKey) *tcell.EventKey { +func (v *LiveView) toggleRefreshCmd(*tcell.EventKey) *tcell.EventKey { v.autoRefresh = !v.autoRefresh if v.autoRefresh { v.Start() @@ -216,9 +216,9 @@ func (v *LiveView) keyboard(evt *tcell.EventKey) *tcell.EventKey { // StylesChanged notifies the skin changed. func (v *LiveView) StylesChanged(s *config.Styles) { - v.SetBackgroundColor(v.app.Styles.BgColor()) - v.text.SetTextColor(v.app.Styles.FgColor()) - v.SetBorderFocusColor(v.app.Styles.Frame().Border.FocusColor.Color()) + v.SetBackgroundColor(s.BgColor()) + v.text.SetTextColor(s.FgColor()) + v.SetBorderFocusColor(s.Frame().Border.FocusColor.Color()) } // Actions returns menu actions. @@ -264,7 +264,7 @@ func (v *LiveView) Hints() model.MenuHints { } // ExtraHints returns additional hints. -func (v *LiveView) ExtraHints() map[string]string { +func (*LiveView) ExtraHints() map[string]string { return nil } @@ -332,7 +332,7 @@ func (v *LiveView) prevCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (v *LiveView) filterCmd(evt *tcell.EventKey) *tcell.EventKey { +func (v *LiveView) filterCmd(*tcell.EventKey) *tcell.EventKey { v.model.Filter(v.cmdBuff.GetText()) v.cmdBuff.SetActive(false) v.updateTitle() @@ -349,7 +349,7 @@ func (v *LiveView) activateCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (v *LiveView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { +func (v *LiveView) eraseCmd(*tcell.EventKey) *tcell.EventKey { if !v.cmdBuff.IsActive() { return nil } @@ -374,7 +374,7 @@ func (v *LiveView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (v *LiveView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { +func (v *LiveView) saveCmd(*tcell.EventKey) *tcell.EventKey { name := fmt.Sprintf("%s--%s", strings.Replace(v.model.GetPath(), "/", "-", 1), strings.ToLower(v.title)) if _, err := saveYAML(v.app.Config.K9s.ContextScreenDumpDir(), name, sanitizeEsc(v.text.GetText(true))); err != nil { v.app.Flash().Err(err) @@ -394,9 +394,12 @@ func (v *LiveView) updateTitle() { fmat = fmt.Sprintf(liveViewTitleFmt, v.title, v.model.GetPath()) } - buff := v.cmdBuff.GetText() + var ( + buff = v.cmdBuff.GetText() + styles = v.app.Styles.Frame() + ) if buff == "" { - v.SetTitle(ui.SkinTitle(fmat, v.app.Styles.Frame())) + v.SetTitle(ui.SkinTitle(fmat, &styles)) return } @@ -404,5 +407,5 @@ func (v *LiveView) updateTitle() { buff += fmt.Sprintf("[%d:%d]", v.currentRegion+1, v.maxRegions) } fmat += fmt.Sprintf(ui.SearchFmt, buff) - v.SetTitle(ui.SkinTitle(fmat, v.app.Styles.Frame())) + v.SetTitle(ui.SkinTitle(fmat, &styles)) } diff --git a/internal/view/live_view_test.go b/internal/view/live_view_test.go index 923ed3a8..d3771bdc 100644 --- a/internal/view/live_view_test.go +++ b/internal/view/live_view_test.go @@ -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" ) func TestLiveViewSetText(t *testing.T) { @@ -20,7 +21,7 @@ apiVersion: v1 ` v := NewLiveView(NewApp(mock.NewMockConfig()), "fred", nil) - assert.NoError(t, v.Init(context.Background())) + require.NoError(t, v.Init(context.Background())) v.text.SetText(colorizeYAML(config.Yaml{}, s)) assert.Equal(t, s, sanitizeEsc(v.text.GetText(true))) diff --git a/internal/view/log.go b/internal/view/log.go index 2134ea62..5bd71783 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -54,7 +54,7 @@ type Log struct { var _ model.Component = (*Log)(nil) // NewLog returns a new viewer. -func NewLog(gvr client.GVR, opts *dao.LogOptions) *Log { +func NewLog(gvr *client.GVR, opts *dao.LogOptions) *Log { l := Log{ Flex: tview.NewFlex(), model: model.NewLog(gvr, opts, defaultFlushTimeout), @@ -86,8 +86,8 @@ func (l *Log) Init(ctx context.Context) (err error) { l.indicator.Refresh() l.logs = NewLogger(l.app) - if err = l.logs.Init(ctx); err != nil { - return err + if e := l.logs.Init(ctx); e != nil { + return e } l.logs.SetBorderPadding(0, 0, 1, 1) l.logs.SetText("[orange::d]" + logMessage) @@ -174,7 +174,7 @@ func (l *Log) BufferCompleted(text, _ string) { } // BufferChanged indicates the buffer was changed. -func (l *Log) BufferChanged(_, _ string) {} +func (*Log) BufferChanged(_, _ string) {} // BufferActive indicates the buff activity changed. func (l *Log) BufferActive(state bool, k model.BufferKind) { @@ -199,7 +199,7 @@ func (l *Log) Hints() model.MenuHints { } // ExtraHints returns additional hints. -func (l *Log) ExtraHints() map[string]string { +func (*Log) ExtraHints() map[string]string { return nil } @@ -241,7 +241,7 @@ func (l *Log) Stop() { } // Name returns the component name. -func (l *Log) Name() string { return logTitle } +func (*Log) Name() string { return logTitle } func (l *Log) bindKeys() { l.logs.Actions().Bulk(ui.KeyMap{ @@ -316,16 +316,19 @@ func (l *Log) updateTitle() { if l.model.LogOptions().Previous { title = " Previous Logs" } - path, co := l.model.GetPath(), l.model.GetContainer() + var ( + path, co = l.model.GetPath(), l.model.GetContainer() + styles = l.app.Styles.Frame() + ) if co == "" { - title += ui.SkinTitle(fmt.Sprintf(logFmt, path, since), l.app.Styles.Frame()) + title += ui.SkinTitle(fmt.Sprintf(logFmt, path, since), &styles) } else { - title += ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co, since), l.app.Styles.Frame()) + title += ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co, since), &styles) } buff := l.logs.cmdBuff.GetText() if buff != "" { - title += ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), l.app.Styles.Frame()) + title += ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), &styles) } l.SetTitle(title) } @@ -352,7 +355,7 @@ func (l *Log) Flush(lines [][]byte) { if l.requestOneRefresh { l.requestOneRefresh = false } - for i := 0; i < len(lines); i++ { + for i := range lines { if l.cancelUpdates { break } @@ -367,7 +370,7 @@ func (l *Log) Flush(lines [][]byte) { // Actions... func (l *Log) sinceCmd(n int) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { + return func(*tcell.EventKey) *tcell.EventKey { l.logs.Clear() ctx := l.getContext() if n == 0 { diff --git a/internal/view/log_indicator_test.go b/internal/view/log_indicator_test.go index 0c793f59..a49d658a 100644 --- a/internal/view/log_indicator_test.go +++ b/internal/view/log_indicator_test.go @@ -40,7 +40,7 @@ func BenchmarkLogIndicatorRefresh(b *testing.B) { b.ReportAllocs() b.ResetTimer() - for n := 0; n < b.N; n++ { + for range b.N { v.Refresh() } } diff --git a/internal/view/log_int_test.go b/internal/view/log_int_test.go index f6b5e4ae..64b3ae22 100644 --- a/internal/view/log_int_test.go +++ b/internal/view/log_int_test.go @@ -11,6 +11,7 @@ import ( "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestLogAutoScroll(t *testing.T) { @@ -19,14 +20,14 @@ func TestLogAutoScroll(t *testing.T) { Container: "blee", SingleContainer: true, } - v := NewLog(client.NewGVR("v1/pods"), &opts) - assert.NoError(t, v.Init(makeContext())) + v := NewLog(client.PodGVR, &opts) + require.NoError(t, v.Init(makeContext())) ii := dao.NewLogItems() ii.Add(dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")) v.GetModel().Set(ii) v.GetModel().Notify() - assert.Equal(t, 16, len(v.Hints())) + assert.Len(t, v.Hints(), 16) v.toggleAutoScrollCmd(nil) assert.Equal(t, "Autoscroll:Off FullScreen:Off Timestamps:Off Wrap:Off", v.Indicator().GetText(true)) @@ -37,11 +38,11 @@ func TestLogViewNav(t *testing.T) { Path: "fred/p1", Container: "blee", } - v := NewLog(client.NewGVR("v1/pods"), &opts) - assert.NoError(t, v.Init(makeContext())) + v := NewLog(client.PodGVR, &opts) + require.NoError(t, v.Init(makeContext())) buff := dao.NewLogItems() - for i := 0; i < 100; i++ { + for i := range 100 { buff.Add(dao.NewLogItemFromString(fmt.Sprintf("line-%d\n", i))) } v.GetModel().Set(buff) @@ -56,14 +57,14 @@ func TestLogViewClear(t *testing.T) { Path: "fred/p1", Container: "blee", } - v := NewLog(client.NewGVR("v1/pods"), &opts) - assert.NoError(t, v.Init(makeContext())) + v := NewLog(client.PodGVR, &opts) + require.NoError(t, v.Init(makeContext())) v.toggleAutoScrollCmd(nil) v.Logs().SetText("blee\nblah") v.Logs().Clear() - assert.Equal(t, "", v.Logs().GetText(true)) + assert.Empty(t, v.Logs().GetText(true)) } func TestLogTimestamp(t *testing.T) { @@ -72,7 +73,7 @@ func TestLogTimestamp(t *testing.T) { Container: "c1", } l := NewLog(client.NewGVR("test"), &opts) - assert.NoError(t, l.Init(makeContext())) + require.NoError(t, l.Init(makeContext())) ii := dao.NewLogItems() ii.Add( &dao.LogItem{ @@ -102,7 +103,7 @@ func TestLogFilter(t *testing.T) { Container: "c1", } l := NewLog(client.NewGVR("test"), &opts) - assert.NoError(t, l.Init(makeContext())) + require.NoError(t, l.Init(makeContext())) buff := dao.NewLogItems() buff.Add( dao.NewLogItemFromString("duh"), @@ -135,8 +136,8 @@ func (l *logList) LogChanged(ll [][]byte) { l.lines += string(line) } } -func (l *logList) LogCanceled() {} -func (l *logList) LogStop() {} -func (l *logList) LogResume() {} +func (*logList) LogCanceled() {} +func (*logList) LogStop() {} +func (*logList) LogResume() {} func (l *logList) LogCleared() { l.clear++ } func (l *logList) LogFailed(error) { l.fail++ } diff --git a/internal/view/log_test.go b/internal/view/log_test.go index fc463d1a..0708960a 100644 --- a/internal/view/log_test.go +++ b/internal/view/log_test.go @@ -19,6 +19,7 @@ import ( "github.com/derailed/k9s/internal/view" "github.com/derailed/tview" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestLog(t *testing.T) { @@ -26,8 +27,8 @@ func TestLog(t *testing.T) { Path: "fred/p1", Container: "blee", } - v := view.NewLog(client.NewGVR("v1/pods"), &opts) - assert.NoError(t, v.Init(makeContext())) + v := view.NewLog(client.PodGVR, &opts) + require.NoError(t, v.Init(makeContext())) ii := dao.NewLogItems() ii.Add(dao.NewLogItemFromString("blee\n"), dao.NewLogItemFromString("bozo\n")) @@ -43,8 +44,8 @@ func TestLogFlush(t *testing.T) { Path: "fred/p1", Container: "blee", } - v := view.NewLog(client.NewGVR("v1/pods"), &opts) - assert.NoError(t, v.Init(makeContext())) + v := view.NewLog(client.PodGVR, &opts) + require.NoError(t, v.Init(makeContext())) items := dao.NewLogItems() items.Add( @@ -63,7 +64,7 @@ func BenchmarkLogFlush(b *testing.B) { Path: "fred/p1", Container: "blee", } - v := view.NewLog(client.NewGVR("v1/pods"), &opts) + v := view.NewLog(client.PodGVR, &opts) _ = v.Init(makeContext()) items := dao.NewLogItems() @@ -77,7 +78,7 @@ func BenchmarkLogFlush(b *testing.B) { b.ReportAllocs() b.ResetTimer() - for n := 0; n < b.N; n++ { + for range b.N { v.Flush(ll) } } @@ -101,8 +102,8 @@ func TestLogViewSave(t *testing.T) { Path: "fred/p1", Container: "blee", } - v := view.NewLog(client.NewGVR("v1/pods"), &opts) - assert.NoError(t, v.Init(makeContext())) + v := view.NewLog(client.PodGVR, &opts) + require.NoError(t, v.Init(makeContext())) app := makeApp() ii := dao.NewLogItems() @@ -112,15 +113,15 @@ func TestLogViewSave(t *testing.T) { v.Flush(ll) dd := "/tmp/test-dumps/na" - assert.NoError(t, ensureDumpDir(dd)) + require.NoError(t, ensureDumpDir(dd)) app.Config.K9s.ScreenDumpDir = "/tmp/test-dumps" dir := app.Config.K9s.ContextScreenDumpDir() c1, err := os.ReadDir(dir) - assert.NoError(t, err, fmt.Sprintf("Dir: %q", dir)) + require.NoError(t, err, "Dir: %q", dir) v.SaveCmd(nil) c2, err := os.ReadDir(dir) - assert.NoError(t, err, fmt.Sprintf("Dir: %q", dir)) - assert.Equal(t, len(c2), len(c1)+1) + require.NoError(t, err, "Dir: %q", dir) + assert.Len(t, c2, len(c1)+1) } func TestAllContainerKeyBinding(t *testing.T) { @@ -139,8 +140,8 @@ func TestAllContainerKeyBinding(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - v := view.NewLog(client.NewGVR("v1/pods"), u.opts) - assert.NoError(t, v.Init(makeContext())) + v := view.NewLog(client.PodGVR, u.opts) + require.NoError(t, v.Init(makeContext())) _, got := v.Logs().Actions().Get(ui.KeyA) assert.Equal(t, u.e, got) }) diff --git a/internal/view/logger.go b/internal/view/logger.go index 7fc649f0..cf56606d 100644 --- a/internal/view/logger.go +++ b/internal/view/logger.go @@ -58,10 +58,10 @@ func (l *Logger) Init(_ context.Context) error { } // BufferChanged indicates the buffer was changed. -func (l *Logger) BufferChanged(_, _ string) {} +func (*Logger) BufferChanged(_, _ string) {} // BufferCompleted indicates input was accepted. -func (l *Logger) BufferCompleted(_, _ string) {} +func (*Logger) BufferCompleted(_, _ string) {} // BufferActive indicates the buff activity changed. func (l *Logger) BufferActive(state bool, k model.BufferKind) { @@ -87,7 +87,7 @@ func (l *Logger) keyboard(evt *tcell.EventKey) *tcell.EventKey { } // StylesChanged notifies the skin changed. -func (l *Logger) StylesChanged(s *config.Styles) { +func (l *Logger) StylesChanged(*config.Styles) { l.SetBackgroundColor(l.app.Styles.BgColor()) l.SetTextColor(l.app.Styles.FgColor()) l.SetBorderFocusColor(l.app.Styles.Frame().Border.FocusColor.Color()) @@ -107,7 +107,7 @@ func (l *Logger) Actions() *ui.KeyActions { func (l *Logger) Name() string { return l.title } // Start starts the view updater. -func (l *Logger) Start() {} +func (*Logger) Start() {} // Stop terminates the updater. func (l *Logger) Stop() { @@ -120,7 +120,7 @@ func (l *Logger) Hints() model.MenuHints { } // ExtraHints returns additional hints. -func (l *Logger) ExtraHints() map[string]string { +func (*Logger) ExtraHints() map[string]string { return nil } @@ -133,7 +133,7 @@ func (l *Logger) activateCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (l *Logger) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { +func (l *Logger) eraseCmd(*tcell.EventKey) *tcell.EventKey { if !l.cmdBuff.IsActive() { return nil } @@ -153,7 +153,7 @@ func (l *Logger) resetCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (l *Logger) saveCmd(evt *tcell.EventKey) *tcell.EventKey { +func (l *Logger) saveCmd(*tcell.EventKey) *tcell.EventKey { if path, err := saveYAML(l.app.Config.K9s.ContextScreenDumpDir(), l.title, l.GetText(true)); err != nil { l.app.Flash().Err(err) } else { diff --git a/internal/view/logs_extender.go b/internal/view/logs_extender.go index d2a8ad4f..a60313bd 100644 --- a/internal/view/logs_extender.go +++ b/internal/view/logs_extender.go @@ -39,7 +39,7 @@ func (l *LogsExtender) bindKeys(aa *ui.KeyActions) { } func (l *LogsExtender) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { + return func(*tcell.EventKey) *tcell.EventKey { path := l.GetTable().GetSelectedItem() if path == "" { return nil @@ -60,7 +60,7 @@ func isResourcePath(p string) bool { func (l *LogsExtender) showLogs(path string, prev bool) { ns, _ := client.Namespaced(path) - _, err := l.App().factory.CanForResource(ns, "v1/pods", client.ListAccess) + _, err := l.App().factory.CanForResource(ns, client.PodGVR, client.ListAccess) if err != nil { l.App().Flash().Err(err) return @@ -83,7 +83,7 @@ func (l *LogsExtender) buildLogOpts(path, co string, prevLogs bool) *dao.LogOpti opts := dao.LogOptions{ Path: path, Container: co, - Lines: int64(cfg.TailCount), + Lines: cfg.TailCount, Previous: prevLogs, ShowTimestamp: cfg.ShowTime, } @@ -94,13 +94,13 @@ func (l *LogsExtender) buildLogOpts(path, co string, prevLogs bool) *dao.LogOpti return &opts } -func podLogOptions(app *App, fqn string, prev bool, m metav1.ObjectMeta, spec v1.PodSpec) *dao.LogOptions { +func podLogOptions(app *App, fqn string, prev bool, m *metav1.ObjectMeta, spec *v1.PodSpec) *dao.LogOptions { var ( cc = fetchContainers(m, spec, true) cfg = app.Config.K9s.Logger opts = dao.LogOptions{ Path: fqn, - Lines: int64(cfg.TailCount), + Lines: cfg.TailCount, SinceSeconds: cfg.SinceSeconds, SingleContainer: len(cc) == 1, ShowTimestamp: cfg.ShowTime, diff --git a/internal/view/node.go b/internal/view/node.go index 8d024630..25daea71 100644 --- a/internal/view/node.go +++ b/internal/view/node.go @@ -25,7 +25,7 @@ type Node struct { } // NewNode returns a new node view. -func NewNode(gvr client.GVR) ResourceViewer { +func NewNode(gvr *client.GVR) ResourceViewer { n := Node{ ResourceViewer: NewBrowser(gvr), } @@ -91,7 +91,7 @@ func (n *Node) bindKeys(aa *ui.KeyActions) { }) } -func (n *Node) showPods(a *App, _ ui.Tabular, _ client.GVR, path string) { +func (n *Node) showPods(a *App, _ ui.Tabular, _ *client.GVR, path string) { showPods(a, n.GetTable().GetSelectedItem(), client.BlankNamespace, "spec.nodeName="+path) } @@ -156,7 +156,8 @@ func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.Eve } else { msg += fmt.Sprintf("(%d) marked %s?", len(sels), n.GVR().R()) } - dialog.ShowConfirm(n.App().Styles.Dialog(), n.App().Content.Pages, title, msg, func() { + d := n.App().Styles.Dialog() + dialog.ShowConfirm(&d, n.App().Content.Pages, title, msg, func() { res, err := dao.AccessorFor(n.App().factory, n.GVR()) if err != nil { n.App().Flash().Err(err) diff --git a/internal/view/ns.go b/internal/view/ns.go index ffde8a42..6453176b 100644 --- a/internal/view/ns.go +++ b/internal/view/ns.go @@ -21,7 +21,7 @@ type Namespace struct { } // NewNamespace returns a new viewer. -func NewNamespace(gvr client.GVR) ResourceViewer { +func NewNamespace(gvr *client.GVR) ResourceViewer { n := Namespace{ ResourceViewer: NewBrowser(gvr), } @@ -39,12 +39,12 @@ func (n *Namespace) bindKeys(aa *ui.KeyActions) { }) } -func (n *Namespace) switchNs(app *App, _ ui.Tabular, _ client.GVR, path string) { +func (n *Namespace) switchNs(app *App, _ ui.Tabular, _ *client.GVR, path string) { n.useNamespace(path) app.gotoResource("pods", "", false, true) } -func (n *Namespace) useNsCmd(evt *tcell.EventKey) *tcell.EventKey { +func (n *Namespace) useNsCmd(*tcell.EventKey) *tcell.EventKey { path := n.GetTable().GetSelectedItem() if path == "" { return nil diff --git a/internal/view/ns_test.go b/internal/view/ns_test.go index 3c06101f..5ab3e281 100644 --- a/internal/view/ns_test.go +++ b/internal/view/ns_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNSCleanser(t *testing.T) { - ns := view.NewNamespace(client.NewGVR("v1/namespaces")) + ns := view.NewNamespace(client.NsGVR) - assert.Nil(t, ns.Init(makeCtx())) + require.NoError(t, ns.Init(makeCtx())) assert.Equal(t, "Namespaces", ns.Name()) - assert.Equal(t, 7, len(ns.Hints())) + assert.Len(t, ns.Hints(), 7) } diff --git a/internal/view/owner_extender.go b/internal/view/owner_extender.go index 1425e88c..f09ba79b 100644 --- a/internal/view/owner_extender.go +++ b/internal/view/owner_extender.go @@ -75,16 +75,17 @@ func (v *OwnerExtender) findOwnerFor(path string) error { ns, _ := client.Namespaced(path) ownerReferences := u.GetOwnerReferences() if len(ownerReferences) == 1 { - return v.jumpOwner(ns, ownerReferences[0]) + return v.jumpOwner(ns, &ownerReferences[0]) } else if len(ownerReferences) > 1 { owners := make([]string, 0, len(ownerReferences)) for idx, ownerRef := range ownerReferences { owners = append(owners, fmt.Sprintf("%d: %s", idx, ownerRef.Kind)) } - dialog.ShowSelection(v.App().Styles.Dialog(), v.App().Content.Pages, "Jump To", owners, func(index int) { + d := v.App().Styles.Dialog() + dialog.ShowSelection(&d, v.App().Content.Pages, "Jump To", owners, func(index int) { if index >= 0 { - err = v.jumpOwner(ns, ownerReferences[index]) + err = v.jumpOwner(ns, &ownerReferences[index]) } }) return err @@ -93,7 +94,7 @@ func (v *OwnerExtender) findOwnerFor(path string) error { return errors.Errorf("no owner found") } -func (v *OwnerExtender) jumpOwner(ns string, owner metav1.OwnerReference) error { +func (v *OwnerExtender) jumpOwner(ns string, owner *metav1.OwnerReference) error { gv, err := schema.ParseGroupVersion(owner.APIVersion) if err != nil { return err @@ -119,7 +120,7 @@ func (v *OwnerExtender) defaultCtx() context.Context { return context.WithValue(context.Background(), internal.KeyFactory, v.App().factory) } -func (v *OwnerExtender) asUnstructuredObject(o runtime.Object) (*unstructured.Unstructured, bool) { +func (*OwnerExtender) asUnstructuredObject(o runtime.Object) (*unstructured.Unstructured, bool) { switch v := o.(type) { case *unstructured.Unstructured: return v, true diff --git a/internal/view/pf.go b/internal/view/pf.go index eaa7c86a..a1c00f24 100644 --- a/internal/view/pf.go +++ b/internal/view/pf.go @@ -29,7 +29,7 @@ type PortForward struct { } // NewPortForward returns a new viewer. -func NewPortForward(gvr client.GVR) ResourceViewer { +func NewPortForward(gvr *client.GVR) ResourceViewer { p := PortForward{ ResourceViewer: NewBrowser(gvr), } @@ -60,8 +60,8 @@ func (p *PortForward) bindKeys(aa *ui.KeyActions) { }) } -func (p *PortForward) showBenchCmd(evt *tcell.EventKey) *tcell.EventKey { - b := NewBenchmark(client.NewGVR("benchmarks")) +func (p *PortForward) showBenchCmd(*tcell.EventKey) *tcell.EventKey { + b := NewBenchmark(client.BeGVR) b.SetContextFn(p.getContext) if err := p.App().inject(b, false); err != nil { p.App().Flash().Err(err) @@ -80,7 +80,7 @@ func (p *PortForward) getContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyPath, path) } -func (p *PortForward) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { +func (p *PortForward) toggleBenchCmd(*tcell.EventKey) *tcell.EventKey { if p.bench != nil { p.App().Status(model.FlashErr, "Benchmark Canceled!") p.bench.Cancel() @@ -103,7 +103,7 @@ func (p *PortForward) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { } base := ui.TrimCell(p.GetTable().SelectTable, r, col) var err error - p.bench, err = perf.NewBenchmark(base, p.App().version, cfg) + p.bench, err = perf.NewBenchmark(base, p.App().version, &cfg) if err != nil { p.App().Flash().Errf("Bench failed %v", err) p.App().ClearStatus(false) @@ -171,10 +171,11 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - dialog.ShowConfirm(p.App().Styles.Dialog(), p.App().Content.Pages, "Delete", msg, func() { + d := p.App().Styles.Dialog() + dialog.ShowConfirm(&d, p.App().Content.Pages, "Delete", msg, func() { for _, s := range selections { var pf dao.PortForward - pf.Init(p.App().factory, client.NewGVR("portforwards")) + pf.Init(p.App().factory, client.PfGVR) if err := pf.Delete(context.Background(), s, nil, dao.DefaultGrace); err != nil { p.App().Flash().Err(err) return diff --git a/internal/view/pf_dialog.go b/internal/view/pf_dialog.go index 5fdbd81b..6bb53024 100644 --- a/internal/view/pf_dialog.go +++ b/internal/view/pf_dialog.go @@ -63,7 +63,7 @@ func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpe f.AddInputField("Address:", address, fieldLen, nil, func(h string) { address = h }) - for i := 0; i < 3; i++ { + for i := range 3 { if field, ok := f.GetFormItem(i).(*tview.InputField); ok { field.SetLabelColor(styles.LabelFgColor.Color()) field.SetFieldTextColor(styles.FieldFgColor.Color()) @@ -88,7 +88,7 @@ func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpe f.AddButton("Cancel", func() { DismissPortForwards(v, pages) }) - for i := 0; i < 2; i++ { + for i := range 2 { if b := f.GetButton(i); b != nil { b.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()) b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color()) @@ -103,7 +103,7 @@ func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpe modal.SetText(msg) modal.SetTextColor(styles.FgColor.Color()) modal.SetBackgroundColor(styles.BgColor.Color()) - modal.SetDoneFunc(func(_ int, b string) { + modal.SetDoneFunc(func(int, string) { DismissPortForwards(v, pages) }) diff --git a/internal/view/pf_extender.go b/internal/view/pf_extender.go index 6a0072a7..893c5c30 100644 --- a/internal/view/pf_extender.go +++ b/internal/view/pf_extender.go @@ -83,7 +83,7 @@ func (p *PortForwardExtender) showPFCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - pf := NewPortForward(client.NewGVR("portforwards")) + pf := NewPortForward(client.PfGVR) pf.SetContextFn(p.portForwardContext) if err := p.App().inject(pf, false); err != nil { p.App().Flash().Err(err) @@ -209,9 +209,9 @@ func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error { return nil } -func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, map[string]string, error) { +func fetchPodPorts(f *watch.Factory, path string) (ports map[string][]v1.ContainerPort, anns map[string]string, err error) { slog.Debug("Fetching ports on pod", slogs.FQN, path) - o, err := f.Get("v1/pods", path, true, labels.Everything()) + o, err := f.Get(client.PodGVR, path, true, labels.Everything()) if err != nil { return nil, nil, err } @@ -223,8 +223,8 @@ func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort } pp := make(map[string][]v1.ContainerPort, len(pod.Spec.Containers)) - for _, co := range pod.Spec.Containers { - pp[co.Name] = co.Ports + for i := range pod.Spec.Containers { + pp[pod.Spec.Containers[i].Name] = pod.Spec.Containers[i].Ports } return pp, pod.Annotations, nil diff --git a/internal/view/pf_extender_test.go b/internal/view/pf_extender_test.go index 23c6b60d..32a5b95d 100644 --- a/internal/view/pf_extender_test.go +++ b/internal/view/pf_extender_test.go @@ -45,8 +45,8 @@ func TestEnsurePodPortFwdAllowed(t *testing.T) { f := testFactory{} if u.podExists { f.expectedGet = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "status": map[string]interface{}{ + Object: map[string]any{ + "status": map[string]any{ "phase": u.podPhase, }, }, @@ -69,34 +69,27 @@ type testFactory struct { var _ dao.Factory = testFactory{} -func (t testFactory) Client() client.Connection { +func (testFactory) Client() client.Connection { return nil } - -func (t testFactory) Get(string, string, bool, labels.Selector) (runtime.Object, error) { +func (t testFactory) Get(*client.GVR, string, bool, labels.Selector) (runtime.Object, error) { if t.expectedGet != nil { return t.expectedGet, nil } return nil, errors.New("not found") } - -func (t testFactory) List(string, string, bool, labels.Selector) ([]runtime.Object, error) { +func (testFactory) List(*client.GVR, string, bool, labels.Selector) ([]runtime.Object, error) { return nil, nil } - -func (t testFactory) ForResource(string, string) (informers.GenericInformer, error) { +func (testFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) { return nil, nil } - -func (t testFactory) CanForResource(string, string, []string) (informers.GenericInformer, error) { +func (testFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) { return nil, nil } - -func (t testFactory) Forwarders() watch.Forwarders { +func (testFactory) Forwarders() watch.Forwarders { return nil } - -func (t testFactory) WaitForCacheSync() {} - -func (t testFactory) DeleteForwarder(string) {} +func (testFactory) WaitForCacheSync() {} +func (testFactory) DeleteForwarder(string) {} diff --git a/internal/view/pf_test.go b/internal/view/pf_test.go index 245e5939..ef04b9e3 100644 --- a/internal/view/pf_test.go +++ b/internal/view/pf_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPortForwardNew(t *testing.T) { - pf := view.NewPortForward(client.NewGVR("portforwards")) + pf := view.NewPortForward(client.PfGVR) - assert.Nil(t, pf.Init(makeCtx())) + require.NoError(t, pf.Init(makeCtx())) assert.Equal(t, "PortForwards", pf.Name()) - assert.Equal(t, 10, len(pf.Hints())) + assert.Len(t, pf.Hints(), 10) } diff --git a/internal/view/picker.go b/internal/view/picker.go index aa2e5b46..9abb4b7a 100644 --- a/internal/view/picker.go +++ b/internal/view/picker.go @@ -28,9 +28,9 @@ func NewPicker() *Picker { } } -func (p *Picker) SetCommand(*cmd.Interpreter) {} -func (p *Picker) SetFilter(string) {} -func (p *Picker) SetLabelFilter(map[string]string) {} +func (*Picker) SetCommand(*cmd.Interpreter) {} +func (*Picker) SetFilter(string) {} +func (*Picker) SetLabelFilter(map[string]string) {} // Init initializes the view. func (p *Picker) Init(ctx context.Context) error { @@ -66,13 +66,13 @@ func (*Picker) InCmdMode() bool { } // Start starts the view. -func (p *Picker) Start() {} +func (*Picker) Start() {} // Stop stops the view. -func (p *Picker) Stop() {} +func (*Picker) Stop() {} // Name returns the component name. -func (p *Picker) Name() string { return "picker" } +func (*Picker) Name() string { return "picker" } // Hints returns the view hints. func (p *Picker) Hints() model.MenuHints { @@ -80,7 +80,7 @@ func (p *Picker) Hints() model.MenuHints { } // ExtraHints returns additional hints. -func (p *Picker) ExtraHints() map[string]string { +func (*Picker) ExtraHints() map[string]string { return nil } diff --git a/internal/view/pod.go b/internal/view/pod.go index cff95690..5782ff0f 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -48,7 +48,7 @@ type Pod struct { } // NewPod returns a new viewer. -func NewPod(gvr client.GVR) ResourceViewer { +func NewPod(gvr *client.GVR) ResourceViewer { var p Pod p.ResourceViewer = NewPortForwardExtender( NewOwnerExtender( @@ -150,11 +150,11 @@ func (p *Pod) logOptions(prev bool) (*dao.LogOptions, error) { return nil, err } - return podLogOptions(p.App(), path, prev, pod.ObjectMeta, pod.Spec), nil + return podLogOptions(p.App(), path, prev, &pod.ObjectMeta, &pod.Spec), nil } -func (p *Pod) showContainers(app *App, _ ui.Tabular, _ client.GVR, _ string) { - co := NewContainer(client.NewGVR("containers")) +func (p *Pod) showContainers(app *App, _ ui.Tabular, _ *client.GVR, _ string) { + co := NewContainer(client.CoGVR) co.SetContextFn(p.coContext) if err := app.inject(co, false); err != nil { app.Flash().Err(err) @@ -181,9 +181,8 @@ func (p *Pod) showNode(evt *tcell.EventKey) *tcell.EventKey { p.App().Flash().Err(errors.New("no node assigned")) return nil } - no := NewNode(client.NewGVR("v1/nodes")) + no := NewNode(client.NodeGVR) no.SetInstance(pod.Spec.NodeName) - //no.SetContextFn(nodeContext(pod.Spec.NodeName)) if err := p.App().inject(no, false); err != nil { p.App().Flash().Err(err) } @@ -262,7 +261,7 @@ func (p *Pod) attachCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (p *Pod) sanitizeCmd(evt *tcell.EventKey) *tcell.EventKey { +func (p *Pod) sanitizeCmd(*tcell.EventKey) *tcell.EventKey { res, err := dao.AccessorFor(p.App().factory, p.GVR()) if err != nil { p.App().Flash().Err(err) @@ -290,7 +289,7 @@ func (p *Pod) sanitizeCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (p *Pod) transferCmd(evt *tcell.EventKey) *tcell.EventKey { +func (p *Pod) transferCmd(*tcell.EventKey) *tcell.EventKey { path := p.GetTable().GetSelectedItem() if path == "" { return nil @@ -308,11 +307,13 @@ func (p *Pod) transferCmd(evt *tcell.EventKey) *tcell.EventKey { } opts := make([]string, 0, 10) - opts = append(opts, "cp") - opts = append(opts, strings.TrimSpace(args.From)) - opts = append(opts, strings.TrimSpace(args.To)) - opts = append(opts, fmt.Sprintf("--no-preserve=%t", args.NoPreserve)) - opts = append(opts, fmt.Sprintf("--retries=%d", args.Retries)) + opts = append(opts, + "cp", + strings.TrimSpace(args.From), + strings.TrimSpace(args.To), + fmt.Sprintf("--no-preserve=%t", args.NoPreserve), + fmt.Sprintf("--retries=%d", args.Retries), + ) if args.CO != "" { opts = append(opts, "-c="+args.CO) } @@ -328,7 +329,7 @@ func (p *Pod) transferCmd(evt *tcell.EventKey) *tcell.EventKey { } fqn := path + ":" + args.CO - if err := runK(p.App(), cliOpts); err != nil { + if err := runK(p.App(), &cliOpts); err != nil { p.App().cowCmd(err.Error()) } else { p.App().Flash().Infof("%s successful on %s!", op, fqn) @@ -344,14 +345,15 @@ func (p *Pod) transferCmd(evt *tcell.EventKey) *tcell.EventKey { opts := dialog.TransferDialogOpts{ Title: "Transfer", - Containers: fetchContainers(pod.ObjectMeta, pod.Spec, false), + Containers: fetchContainers(&pod.ObjectMeta, &pod.Spec, false), Message: "Download Files", Pod: fmt.Sprintf("%s/%s:", ns, n), Ack: ack, Retries: defaultTxRetries, Cancel: func() {}, } - dialog.ShowUploads(p.App().Styles.Dialog(), p.App().Content.Pages, opts) + d := p.App().Styles.Dialog() + dialog.ShowUploads(&d, p.App().Content.Pages, &opts) return nil } @@ -369,12 +371,12 @@ func containerShellIn(a *App, comp model.Component, path, co string) error { if err != nil { return err } - if dco, ok := dao.GetDefaultContainer(pod.ObjectMeta, pod.Spec); ok { + if dco, ok := dao.GetDefaultContainer(&pod.ObjectMeta, &pod.Spec); ok { resumeShellIn(a, comp, path, dco) return nil } - cc := fetchContainers(pod.ObjectMeta, pod.Spec, false) + cc := fetchContainers(&pod.ObjectMeta, &pod.Spec, false) if len(cc) == 1 { resumeShellIn(a, comp, path, cc[0]) return nil @@ -404,7 +406,11 @@ func shellIn(a *App, fqn, co string) { args := computeShellArgs(fqn, co, a.Conn().Config().Flags().KubeConfig, os) c := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold) - err = runK(a, shellOpts{clear: true, banner: c.Sprintf(bannerFmt, fqn, co), args: args}) + err = runK(a, &shellOpts{ + clear: true, + banner: c.Sprintf(bannerFmt, fqn, co), + args: args}, + ) if err != nil { a.Flash().Errf("Shell exec failed: %s", err) } @@ -420,7 +426,7 @@ func containerAttachIn(a *App, comp model.Component, path, co string) error { if err != nil { return err } - cc := fetchContainers(pod.ObjectMeta, pod.Spec, false) + cc := fetchContainers(&pod.ObjectMeta, &pod.Spec, false) if len(cc) == 1 { resumeAttachIn(a, comp, path, cc[0]) return nil @@ -447,7 +453,7 @@ func resumeAttachIn(a *App, c model.Component, path, co string) { func attachIn(a *App, path, co string) { args := buildShellArgs("attach", path, co, a.Conn().Config().Flags().KubeConfig) c := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold) - if err := runK(a, shellOpts{clear: true, banner: c.Sprintf(bannerFmt, path, co), args: args}); err != nil { + if err := runK(a, &shellOpts{clear: true, banner: c.Sprintf(bannerFmt, path, co), args: args}); err != nil { a.Flash().Errf("Attach exec failed: %s", err) } } @@ -478,7 +484,7 @@ func buildShellArgs(cmd, path, co string, kcfg *string) []string { return args } -func fetchContainers(meta metav1.ObjectMeta, spec v1.PodSpec, allContainers bool) []string { +func fetchContainers(meta *metav1.ObjectMeta, spec *v1.PodSpec, allContainers bool) []string { nn := make([]string, 0, len(spec.Containers)+len(spec.InitContainers)) // put the default container as the first entry defaultContainer, ok := dao.GetDefaultContainer(meta, spec) @@ -486,26 +492,27 @@ func fetchContainers(meta metav1.ObjectMeta, spec v1.PodSpec, allContainers bool nn = append(nn, defaultContainer) } - for _, c := range spec.Containers { - if c.Name != defaultContainer { - nn = append(nn, c.Name) + for i := range spec.Containers { + if spec.Containers[i].Name != defaultContainer { + nn = append(nn, spec.Containers[i].Name) } } if !allContainers { return nn } - for _, c := range spec.InitContainers { - nn = append(nn, c.Name) + + for i := range spec.InitContainers { + nn = append(nn, spec.InitContainers[i].Name) } - for _, c := range spec.EphemeralContainers { - nn = append(nn, c.Name) + for i := range spec.EphemeralContainers { + nn = append(nn, spec.EphemeralContainers[i].Name) } return nn } func fetchPod(f dao.Factory, path string) (*v1.Pod, error) { - o, err := f.Get("v1/pods", path, true, labels.Everything()) + o, err := f.Get(client.PodGVR, path, true, labels.Everything()) if err != nil { return nil, err } @@ -530,7 +537,7 @@ func podIsRunning(f dao.Factory, fqn string) bool { } var re render.Pod - return re.Phase(po) == render.Running + return re.Phase(po.DeletionTimestamp, &po.Spec, &po.Status) == render.Running } func getPodOS(f dao.Factory, fqn string) (string, error) { diff --git a/internal/view/pod_test.go b/internal/view/pod_test.go index 23bdebaf..c1aee845 100644 --- a/internal/view/pod_test.go +++ b/internal/view/pod_test.go @@ -12,14 +12,15 @@ import ( "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPodNew(t *testing.T) { - po := view.NewPod(client.NewGVR("v1/pods")) + po := view.NewPod(client.PodGVR) - assert.Nil(t, po.Init(makeCtx())) + require.NoError(t, po.Init(makeCtx())) assert.Equal(t, "Pods", po.Name()) - assert.Equal(t, 28, len(po.Hints())) + assert.Len(t, po.Hints(), 28) } // Helpers... diff --git a/internal/view/policy.go b/internal/view/policy.go index b412b388..92a92324 100644 --- a/internal/view/policy.go +++ b/internal/view/policy.go @@ -26,9 +26,9 @@ type Policy struct { } // NewPolicy returns a new viewer. -func NewPolicy(app *App, subject, name string) *Policy { +func NewPolicy(_ *App, subject, name string) *Policy { p := Policy{ - ResourceViewer: NewBrowser(client.NewGVR("policy")), + ResourceViewer: NewBrowser(client.PolGVR), subjectKind: subject, subjectName: name, } diff --git a/internal/view/popeye.go b/internal/view/popeye.go deleted file mode 100644 index e3c6355b..00000000 --- a/internal/view/popeye.go +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package view - -// import ( -// "context" -// "fmt" -// "strconv" -// "time" - -// "github.com/derailed/k9s/internal" -// "github.com/derailed/k9s/internal/client" -// "github.com/derailed/k9s/internal/render" -// "github.com/derailed/k9s/internal/ui" -// "github.com/derailed/tcell/v2" -// ) - -// // Popeye represents a sanitizer view. -// type Popeye struct { -// ResourceViewer -// } - -// // NewPopeye returns a new view. -// func NewPopeye(gvr client.GVR) ResourceViewer { -// p := Popeye{ -// ResourceViewer: NewBrowser(gvr), -// } -// p.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) -// p.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone)) -// p.GetTable().SetSortCol("SCORE%", true) -// p.GetTable().SetDecorateFn(p.decorateRows) -// p.AddBindKeysFn(p.bindKeys) - -// return &p -// } - -// // Init initializes the view. -// func (p *Popeye) Init(ctx context.Context) error { -// if err := p.ResourceViewer.Init(ctx); err != nil { -// return err -// } -// p.GetTable().GetModel().SetRefreshRate(5 * time.Second) - -// return nil -// } - -// func (p *Popeye) decorateRows(data *model1.TableData) { -// var sum int -// for _, re := range data.RowEvents { -// n, err := strconv.Atoi(re.Row.Fields[1]) -// if err != nil { -// continue -// } -// sum += n -// } -// score, letter := 0, render.NAValue -// if len(data.RowEvents) > 0 { -// score = sum / len(data.RowEvents) -// letter = grade(score) -// } -// p.GetTable().Extras = fmt.Sprintf("Score %d -- %s", score, letter) -// } - -// func (p *Popeye) bindKeys(aa ui.KeyActions) { -// aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) -// aa.Add(ui.KeyActions{ -// tcell.KeyEnter: ui.NewKeyAction("Goto", p.gotoCmd, true), -// ui.KeyShiftR: ui.NewKeyAction("Sort Resource", p.GetTable().SortColCmd("RESOURCE", true), false), -// ui.KeyShiftS: ui.NewKeyAction("Sort Score", p.GetTable().SortColCmd("SCORE%", true), false), -// ui.KeyShiftO: ui.NewKeyAction("Sort OK", p.GetTable().SortColCmd("OK", true), false), -// ui.KeyShiftI: ui.NewKeyAction("Sort Info", p.GetTable().SortColCmd("INFO", true), false), -// ui.KeyShiftW: ui.NewKeyAction("Sort Warning", p.GetTable().SortColCmd("WARNING", true), false), -// ui.KeyShiftE: ui.NewKeyAction("Sort Error", p.GetTable().SortColCmd("ERROR", true), false), -// }) -// } - -// func (p *Popeye) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { -// path := p.GetTable().GetSelectedItem() -// if path == "" { -// return evt -// } -// v := NewSanitizer(client.NewGVR("sanitizer")) -// v.SetContextFn(sanitizerCtx(path)) -// if err := p.App().inject(v, false); err != nil { -// p.App().Flash().Err(err) -// } - -// return nil -// } - -// func sanitizerCtx(path string) ContextFunc { -// return func(ctx context.Context) context.Context { -// ctx = context.WithValue(ctx, internal.KeyPath, path) -// return ctx -// } -// } - -// // Helpers... - -// func grade(score int) string { -// switch { -// case score >= 90: -// return "A" -// case score >= 80: -// return "B" -// case score >= 70: -// return "C" -// case score >= 60: -// return "D" -// case score >= 50: -// return "E" -// default: -// return "F" -// } -// } diff --git a/internal/view/priorityclass.go b/internal/view/priorityclass.go index 7f9ef909..dec0b0a2 100644 --- a/internal/view/priorityclass.go +++ b/internal/view/priorityclass.go @@ -5,7 +5,6 @@ package view import ( "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" ) @@ -16,7 +15,7 @@ type PriorityClass struct { } // NewPriorityClass returns a new viewer. -func NewPriorityClass(gvr client.GVR) ResourceViewer { +func NewPriorityClass(gvr *client.GVR) ResourceViewer { s := PriorityClass{ ResourceViewer: NewBrowser(gvr), } @@ -30,5 +29,5 @@ func (s *PriorityClass) bindKeys(aa *ui.KeyActions) { } func (s *PriorityClass) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanRefs(evt, s.App(), s.GetTable(), dao.PcGVR) + return scanRefs(evt, s.App(), s.GetTable(), client.PcGVR) } diff --git a/internal/view/priorityclass_test.go b/internal/view/priorityclass_test.go index 60cef2ab..0819ce67 100644 --- a/internal/view/priorityclass_test.go +++ b/internal/view/priorityclass_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPriorityClassNew(t *testing.T) { - s := view.NewPriorityClass(client.NewGVR("scheduling.k8s.io/v1/priorityclasses")) + s := view.NewPriorityClass(client.PcGVR) - assert.Nil(t, s.Init(makeCtx())) + require.NoError(t, s.Init(makeCtx())) assert.Equal(t, "PriorityClass", s.Name()) - assert.Equal(t, 6, len(s.Hints())) + assert.Len(t, s.Hints(), 6) } diff --git a/internal/view/pulse.go b/internal/view/pulse.go index 18500987..81c140ad 100644 --- a/internal/view/pulse.go +++ b/internal/view/pulse.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "image" + "strings" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" @@ -61,7 +62,7 @@ type Pulse struct { *tview.Grid app *App - gvr client.GVR + gvr *client.GVR model *model.Pulse cancelFn context.CancelFunc actions *ui.KeyActions @@ -69,7 +70,7 @@ type Pulse struct { } // NewPulse returns a new alias view. -func NewPulse(gvr client.GVR) ResourceViewer { +func NewPulse(gvr *client.GVR) ResourceViewer { return &Pulse{ Grid: tview.NewGrid(), model: model.NewPulse(gvr.String()), @@ -77,9 +78,9 @@ func NewPulse(gvr client.GVR) ResourceViewer { } } -func (p *Pulse) SetCommand(*cmd.Interpreter) {} -func (p *Pulse) SetFilter(string) {} -func (p *Pulse) SetLabelFilter(map[string]string) {} +func (*Pulse) SetCommand(*cmd.Interpreter) {} +func (*Pulse) SetFilter(string) {} +func (*Pulse) SetLabelFilter(map[string]string) {} // Init initializes the view. func (p *Pulse) Init(ctx context.Context) error { @@ -93,19 +94,19 @@ func (p *Pulse) Init(ctx context.Context) error { } p.charts = []Graphable{ - p.makeGA(image.Point{X: 0, Y: 0}, image.Point{X: 2, Y: 2}, "apps/v1/deployments"), - p.makeGA(image.Point{X: 0, Y: 2}, image.Point{X: 2, Y: 2}, "apps/v1/replicasets"), - p.makeGA(image.Point{X: 0, Y: 4}, image.Point{X: 2, Y: 2}, "apps/v1/statefulsets"), - p.makeGA(image.Point{X: 0, Y: 6}, image.Point{X: 2, Y: 2}, "apps/v1/daemonsets"), - p.makeSP(image.Point{X: 2, Y: 0}, image.Point{X: 3, Y: 2}, "v1/pods"), - p.makeSP(image.Point{X: 2, Y: 2}, image.Point{X: 3, Y: 2}, "v1/events"), - p.makeSP(image.Point{X: 2, Y: 4}, image.Point{X: 3, Y: 2}, "batch/v1/jobs"), - p.makeSP(image.Point{X: 2, Y: 6}, image.Point{X: 3, Y: 2}, "v1/persistentvolumes"), + p.makeGA(image.Point{X: 0, Y: 0}, image.Point{X: 2, Y: 2}, client.DpGVR), + p.makeGA(image.Point{X: 0, Y: 2}, image.Point{X: 2, Y: 2}, client.RsGVR), + p.makeGA(image.Point{X: 0, Y: 4}, image.Point{X: 2, Y: 2}, client.StsGVR), + p.makeGA(image.Point{X: 0, Y: 6}, image.Point{X: 2, Y: 2}, client.DsGVR), + p.makeSP(image.Point{X: 2, Y: 0}, image.Point{X: 3, Y: 2}, client.PodGVR), + p.makeSP(image.Point{X: 2, Y: 2}, image.Point{X: 3, Y: 2}, client.EvGVR), + p.makeSP(image.Point{X: 2, Y: 4}, image.Point{X: 3, Y: 2}, client.JobGVR), + p.makeSP(image.Point{X: 2, Y: 6}, image.Point{X: 3, Y: 2}, client.PvGVR), } if p.app.Conn().HasMetrics() { p.charts = append(p.charts, - p.makeSP(image.Point{X: 5, Y: 0}, image.Point{X: 2, Y: 4}, "cpu"), - p.makeSP(image.Point{X: 5, Y: 4}, image.Point{X: 2, Y: 4}, "mem"), + p.makeSP(image.Point{X: 5, Y: 0}, image.Point{X: 2, Y: 4}, client.CpuGVR), + p.makeSP(image.Point{X: 5, Y: 4}, image.Point{X: 2, Y: 4}, client.MemGVR), ) } p.bindKeys() @@ -166,24 +167,23 @@ func (p *Pulse) PulseChanged(c *health.Check) { nn[1] = "gray" } - gvr := client.NewGVR(c.GVR) switch c.GVR { - case "cpu": + case client.CpuGVR: perc := client.ToPercentage(c.Tally(health.S1), c.Tally(health.S2)) v.SetLegend(fmt.Sprintf(cpuFmt, - cases.Title(language.Und, cases.NoLower).String(gvr.R()), - p.app.Config.K9s.Thresholds.SeverityColor("cpu", perc), + cases.Title(language.Und, cases.NoLower).String(c.GVR.R()), + p.app.Config.K9s.Thresholds.SeverityColor(config.CPU, perc), render.PrintPerc(perc), nn[0], render.AsThousands(c.Tally(health.S1)), nn[1], render.AsThousands(c.Tally(health.S2)), )) - case "mem": + case client.MemGVR: perc := client.ToPercentage(c.Tally(health.S1), c.Tally(health.S2)) v.SetLegend(fmt.Sprintf(memFmt, - cases.Title(language.Und, cases.NoLower).String(gvr.R()), - p.app.Config.K9s.Thresholds.SeverityColor("memory", perc), + cases.Title(language.Und, cases.NoLower).String(c.GVR.R()), + p.app.Config.K9s.Thresholds.SeverityColor(config.MEM, perc), render.PrintPerc(perc), nn[0], render.AsThousands(c.Tally(health.S1)), @@ -192,7 +192,7 @@ func (p *Pulse) PulseChanged(c *health.Check) { )) default: v.SetLegend(fmt.Sprintf(genFmat, - cases.Title(language.Und, cases.NoLower).String(gvr.R()), + cases.Title(language.Und, cases.NoLower).String(c.GVR.R()), nn[0], c.Tally(health.S1), nn[1], @@ -215,7 +215,8 @@ func (p *Pulse) bindKeys() { })) for i, v := range p.charts { - t := cases.Title(language.Und, cases.NoLower).String(client.NewGVR(v.ID()).R()) + tt := strings.Split(v.ID(), "/") + t := cases.Title(language.Und, cases.NoLower).String(tt[len(tt)-1]) p.actions.Add(ui.NumKeys[i], ui.NewKeyAction(t, p.sparkFocusCmd(i), true)) } } @@ -255,15 +256,15 @@ func (p *Pulse) Stop() { } // Refresh updates the view. -func (p *Pulse) Refresh() {} +func (*Pulse) Refresh() {} // GVR returns a resource descriptor. -func (p *Pulse) GVR() client.GVR { +func (p *Pulse) GVR() *client.GVR { return p.gvr } // Name returns the component name. -func (p *Pulse) Name() string { +func (*Pulse) Name() string { return pulseTitle } @@ -273,19 +274,19 @@ func (p *Pulse) App() *App { } // SetInstance sets specific resource instance. -func (p *Pulse) SetInstance(string) {} +func (*Pulse) SetInstance(string) {} // SetEnvFn sets the custom environment function. -func (p *Pulse) SetEnvFn(EnvFunc) {} +func (*Pulse) SetEnvFn(EnvFunc) {} // AddBindKeysFn sets up extra key bindings. -func (p *Pulse) AddBindKeysFn(BindKeysFunc) {} +func (*Pulse) AddBindKeysFn(BindKeysFunc) {} // SetContextFn sets custom context. -func (p *Pulse) SetContextFn(ContextFunc) {} +func (*Pulse) SetContextFn(ContextFunc) {} // GetTable return the view table if any. -func (p *Pulse) GetTable() *Table { +func (*Pulse) GetTable() *Table { return nil } @@ -300,26 +301,26 @@ func (p *Pulse) Hints() model.MenuHints { } // ExtraHints returns additional hints. -func (p *Pulse) ExtraHints() map[string]string { +func (*Pulse) ExtraHints() map[string]string { return nil } func (p *Pulse) sparkFocusCmd(i int) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { + return func(*tcell.EventKey) *tcell.EventKey { p.app.SetFocus(p.charts[i]) return nil } } -func (p *Pulse) enterCmd(evt *tcell.EventKey) *tcell.EventKey { +func (p *Pulse) enterCmd(*tcell.EventKey) *tcell.EventKey { v := p.App().GetFocus() s, ok := v.(Graphable) if !ok { return nil } - res := client.NewGVR(s.ID()).R() - if res == "cpu" || res == "mem" { - res = "pod" + res := s.ID() + if res == client.CpuGVR.String() || res == client.MemGVR.String() { + res = client.PodGVR.String() } p.App().gotoResource(res+" all", "", false, true) @@ -327,7 +328,7 @@ func (p *Pulse) enterCmd(evt *tcell.EventKey) *tcell.EventKey { } func (p *Pulse) nextFocusCmd(direction int) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { + return func(*tcell.EventKey) *tcell.EventKey { v := p.app.GetFocus() index := findIndex(p.charts, v) p.GetItem(index).Focus = false @@ -340,16 +341,16 @@ func (p *Pulse) nextFocusCmd(direction int) func(evt *tcell.EventKey) *tcell.Eve } } -func (p *Pulse) makeSP(loc image.Point, span image.Point, gvr string) *tchart.SparkLine { - s := tchart.NewSparkLine(gvr) +func (p *Pulse) makeSP(loc, span image.Point, gvr *client.GVR) *tchart.SparkLine { + s := tchart.NewSparkLine(gvr.String()) s.SetBackgroundColor(p.app.Styles.Charts().BgColor.Color()) s.SetBorderPadding(0, 1, 0, 1) - if cc, ok := p.app.Styles.Charts().ResourceColors[gvr]; ok { + if cc, ok := p.app.Styles.Charts().ResourceColors[gvr.String()]; ok { s.SetSeriesColors(cc.Colors()...) } else { s.SetSeriesColors(p.app.Styles.Charts().DefaultChartColors.Colors()...) } - s.SetLegend(fmt.Sprintf(" %s ", cases.Title(language.Und, cases.NoLower).String(client.NewGVR(gvr).R()))) + s.SetLegend(fmt.Sprintf(" %s ", cases.Title(language.Und, cases.NoLower).String(gvr.R()))) s.SetInputCapture(p.keyboard) s.SetMultiSeries(true) p.AddItem(s, loc.X, loc.Y, span.X, span.Y, 0, 0, true) @@ -357,17 +358,15 @@ func (p *Pulse) makeSP(loc image.Point, span image.Point, gvr string) *tchart.Sp return s } -func (p *Pulse) makeGA(loc image.Point, span image.Point, gvr string) *tchart.Gauge { - g := tchart.NewGauge(gvr) - // g.SetResolution(3) +func (p *Pulse) makeGA(loc, span image.Point, gvr *client.GVR) *tchart.Gauge { + g := tchart.NewGauge(gvr.String()) g.SetBackgroundColor(p.app.Styles.Charts().BgColor.Color()) - // g.SetBorderPadding(0, 1, 0, 1) - if cc, ok := p.app.Styles.Charts().ResourceColors[gvr]; ok { + if cc, ok := p.app.Styles.Charts().ResourceColors[gvr.String()]; ok { g.SetSeriesColors(cc.Colors()...) } else { g.SetSeriesColors(p.app.Styles.Charts().DefaultDialColors.Colors()...) } - g.SetLegend(fmt.Sprintf(" %s ", cases.Title(language.Und, cases.NoLower).String(client.NewGVR(gvr).R()))) + g.SetLegend(fmt.Sprintf(" %s ", cases.Title(language.Und, cases.NoLower).String(gvr.R()))) g.SetInputCapture(p.keyboard) p.AddItem(g, loc.X, loc.Y, span.X, span.Y, 0, 0, true) @@ -398,9 +397,9 @@ func findIndex(pp []Graphable, p tview.Primitive) int { return 0 } -func findIndexGVR(pp []Graphable, gvr string) (int, bool) { +func findIndexGVR(pp []Graphable, gvr *client.GVR) (int, bool) { for i, v := range pp { - if v.ID() == gvr { + if v.ID() == gvr.String() { return i, true } } diff --git a/internal/view/pvc.go b/internal/view/pvc.go index b071ca4c..63a73864 100644 --- a/internal/view/pvc.go +++ b/internal/view/pvc.go @@ -5,7 +5,6 @@ package view import ( "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" ) @@ -16,7 +15,7 @@ type PersistentVolumeClaim struct { } // NewPersistentVolumeClaim returns a new viewer. -func NewPersistentVolumeClaim(gvr client.GVR) ResourceViewer { +func NewPersistentVolumeClaim(gvr *client.GVR) ResourceViewer { v := PersistentVolumeClaim{ ResourceViewer: NewOwnerExtender(NewBrowser(gvr)), } @@ -36,5 +35,5 @@ func (p *PersistentVolumeClaim) bindKeys(aa *ui.KeyActions) { } func (p *PersistentVolumeClaim) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanRefs(evt, p.App(), p.GetTable(), dao.PvcGVR) + return scanRefs(evt, p.App(), p.GetTable(), client.PvcGVR) } diff --git a/internal/view/pvc_test.go b/internal/view/pvc_test.go index 7434729b..bbd7112e 100644 --- a/internal/view/pvc_test.go +++ b/internal/view/pvc_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPVCNew(t *testing.T) { - v := view.NewPersistentVolumeClaim(client.NewGVR("v1/persistentvolumeclaims")) + v := view.NewPersistentVolumeClaim(client.PvcGVR) - assert.Nil(t, v.Init(makeCtx())) + require.NoError(t, v.Init(makeCtx())) assert.Equal(t, "PersistentVolumeClaims", v.Name()) - assert.Equal(t, 11, len(v.Hints())) + assert.Len(t, v.Hints(), 11) } diff --git a/internal/view/rbac.go b/internal/view/rbac.go index 55831228..62f6ecdb 100644 --- a/internal/view/rbac.go +++ b/internal/view/rbac.go @@ -18,7 +18,7 @@ type Rbac struct { } // NewRbac returns a new viewer. -func NewRbac(gvr client.GVR) ResourceViewer { +func NewRbac(gvr *client.GVR) ResourceViewer { r := Rbac{ ResourceViewer: NewBrowser(gvr), } @@ -34,8 +34,8 @@ func (r *Rbac) bindKeys(aa *ui.KeyActions) { aa.Add(ui.KeyShiftA, ui.NewKeyAction("Sort API-Group", r.GetTable().SortColCmd("API-GROUP", true), false)) } -func showRules(app *App, _ ui.Tabular, gvr client.GVR, path string) { - v := NewRbac(client.NewGVR("rbac")) +func showRules(app *App, _ ui.Tabular, gvr *client.GVR, path string) { + v := NewRbac(client.RbacGVR) v.SetContextFn(rbacCtx(gvr, path)) if err := app.inject(v, false); err != nil { @@ -43,11 +43,11 @@ func showRules(app *App, _ ui.Tabular, gvr client.GVR, path string) { } } -func rbacCtx(gvr client.GVR, path string) ContextFunc { +func rbacCtx(gvr *client.GVR, path string) ContextFunc { return func(ctx context.Context) context.Context { ctx = context.WithValue(ctx, internal.KeyPath, path) return context.WithValue(ctx, internal.KeyGVR, gvr) } } -func blankEnterFn(_ *App, _ ui.Tabular, _ client.GVR, _ string) {} +func blankEnterFn(_ *App, _ ui.Tabular, _ *client.GVR, _ string) {} diff --git a/internal/view/rbac_test.go b/internal/view/rbac_test.go index 5d72d358..169ec0c6 100644 --- a/internal/view/rbac_test.go +++ b/internal/view/rbac_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRbacNew(t *testing.T) { - v := view.NewRbac(client.NewGVR("rbac")) + v := view.NewRbac(client.RbacGVR) - assert.Nil(t, v.Init(makeCtx())) + require.NoError(t, v.Init(makeCtx())) assert.Equal(t, "Rbac", v.Name()) - assert.Equal(t, 5, len(v.Hints())) + assert.Len(t, v.Hints(), 5) } diff --git a/internal/view/reference.go b/internal/view/reference.go index d3251e47..da248931 100644 --- a/internal/view/reference.go +++ b/internal/view/reference.go @@ -17,7 +17,7 @@ type Reference struct { } // NewReference returns a new alias view. -func NewReference(gvr client.GVR) ResourceViewer { +func NewReference(gvr *client.GVR) ResourceViewer { r := Reference{ ResourceViewer: NewBrowser(gvr), } diff --git a/internal/view/reference_test.go b/internal/view/reference_test.go index 0e3578f7..359e0037 100644 --- a/internal/view/reference_test.go +++ b/internal/view/reference_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestReferenceNew(t *testing.T) { - s := view.NewReference(client.NewGVR("references")) + s := view.NewReference(client.RefGVR) - assert.Nil(t, s.Init(makeCtx())) + require.NoError(t, s.Init(makeCtx())) assert.Equal(t, "References", s.Name()) - assert.Equal(t, 4, len(s.Hints())) + assert.Len(t, s.Hints(), 4) } diff --git a/internal/view/registrar.go b/internal/view/registrar.go index e40e627a..655a4db6 100644 --- a/internal/view/registrar.go +++ b/internal/view/registrar.go @@ -21,133 +21,127 @@ func loadCustomViewers() MetaViewers { } func helmViewers(vv MetaViewers) { - vv[client.NewGVR("helm")] = MetaViewer{ + vv[client.HmGVR] = MetaViewer{ viewerFn: NewHelmChart, } } func coreViewers(vv MetaViewers) { - vv[client.NewGVR("v1/namespaces")] = MetaViewer{ + vv[client.NsGVR] = MetaViewer{ viewerFn: NewNamespace, } - vv[client.NewGVR("v1/events")] = MetaViewer{ + vv[client.EvGVR] = MetaViewer{ viewerFn: NewEvent, } - vv[client.NewGVR("v1/pods")] = MetaViewer{ + vv[client.PodGVR] = MetaViewer{ viewerFn: NewPod, } - vv[client.NewGVR("v1/services")] = MetaViewer{ + vv[client.SvcGVR] = MetaViewer{ viewerFn: NewService, } - vv[client.NewGVR("v1/nodes")] = MetaViewer{ + vv[client.NodeGVR] = MetaViewer{ viewerFn: NewNode, } - vv[client.NewGVR("v1/secrets")] = MetaViewer{ + vv[client.SecGVR] = MetaViewer{ viewerFn: NewSecret, } - vv[client.NewGVR("scheduling.k8s.io/v1/priorityclasses")] = MetaViewer{ + vv[client.PcGVR] = MetaViewer{ viewerFn: NewPriorityClass, } - vv[client.NewGVR("v1/configmaps")] = MetaViewer{ + vv[client.CmGVR] = MetaViewer{ viewerFn: NewConfigMap, } - vv[client.NewGVR("v1/serviceaccounts")] = MetaViewer{ + vv[client.SaGVR] = MetaViewer{ viewerFn: NewServiceAccount, } - vv[client.NewGVR("v1/persistentvolumeclaims")] = MetaViewer{ + vv[client.PvcGVR] = MetaViewer{ viewerFn: NewPersistentVolumeClaim, } } func miscViewers(vv MetaViewers) { - vv[client.NewGVR("workloads")] = MetaViewer{ + vv[client.WkGVR] = MetaViewer{ viewerFn: NewWorkload, } - vv[client.NewGVR("contexts")] = MetaViewer{ + vv[client.CtGVR] = MetaViewer{ viewerFn: NewContext, } - vv[client.NewGVR("containers")] = MetaViewer{ + vv[client.CoGVR] = MetaViewer{ viewerFn: NewContainer, } - vv[client.NewGVR("scans")] = MetaViewer{ + vv[client.ScGVR] = MetaViewer{ viewerFn: NewImageScan, } - vv[client.NewGVR("portforwards")] = MetaViewer{ + vv[client.PfGVR] = MetaViewer{ viewerFn: NewPortForward, } - vv[client.NewGVR("screendumps")] = MetaViewer{ + vv[client.SdGVR] = MetaViewer{ viewerFn: NewScreenDump, } - vv[client.NewGVR("benchmarks")] = MetaViewer{ + vv[client.BeGVR] = MetaViewer{ viewerFn: NewBenchmark, } - vv[client.NewGVR("aliases")] = MetaViewer{ + vv[client.AliGVR] = MetaViewer{ viewerFn: NewAlias, } - vv[client.NewGVR("references")] = MetaViewer{ + vv[client.RefGVR] = MetaViewer{ viewerFn: NewReference, } - vv[client.NewGVR("pulses")] = MetaViewer{ + vv[client.PuGVR] = MetaViewer{ viewerFn: NewPulse, } } func appsViewers(vv MetaViewers) { - vv[client.NewGVR("apps/v1/deployments")] = MetaViewer{ + vv[client.DpGVR] = MetaViewer{ viewerFn: NewDeploy, } - vv[client.NewGVR("apps/v1/replicasets")] = MetaViewer{ + vv[client.RsGVR] = MetaViewer{ viewerFn: NewReplicaSet, } - vv[client.NewGVR("apps/v1/statefulsets")] = MetaViewer{ + vv[client.StsGVR] = MetaViewer{ viewerFn: NewStatefulSet, } - vv[client.NewGVR("apps/v1/daemonsets")] = MetaViewer{ - viewerFn: NewDaemonSet, - } - vv[client.NewGVR("apps/v1/daemonsets")] = MetaViewer{ + vv[client.DsGVR] = MetaViewer{ viewerFn: NewDaemonSet, } } func rbacViewers(vv MetaViewers) { - vv[client.NewGVR("rbac")] = MetaViewer{ + vv[client.RbacGVR] = MetaViewer{ enterFn: showRules, } - vv[client.NewGVR("users")] = MetaViewer{ + vv[client.UsrGVR] = MetaViewer{ viewerFn: NewUser, } - vv[client.NewGVR("groups")] = MetaViewer{ + vv[client.GrpGVR] = MetaViewer{ viewerFn: NewGroup, } - vv[client.NewGVR("rbac.authorization.k8s.io/v1/clusterroles")] = MetaViewer{ + vv[client.CrGVR] = MetaViewer{ enterFn: showRules, } - vv[client.NewGVR("rbac.authorization.k8s.io/v1/roles")] = MetaViewer{ + vv[client.CrbGVR] = MetaViewer{ enterFn: showRules, } - vv[client.NewGVR("rbac.authorization.k8s.io/v1/clusterrolebindings")] = MetaViewer{ + vv[client.RoGVR] = MetaViewer{ enterFn: showRules, } - vv[client.NewGVR("rbac.authorization.k8s.io/v1/rolebindings")] = MetaViewer{ + vv[client.RobGVR] = MetaViewer{ enterFn: showRules, } } func batchViewers(vv MetaViewers) { - vv[client.NewGVR("batch/v1beta1/cronjobs")] = MetaViewer{ + vv[client.CjGVR] = MetaViewer{ viewerFn: NewCronJob, } - vv[client.NewGVR("batch/v1/cronjobs")] = MetaViewer{ - viewerFn: NewCronJob, - } - vv[client.NewGVR("batch/v1/jobs")] = MetaViewer{ + vv[client.JobGVR] = MetaViewer{ viewerFn: NewJob, } } func crdViewers(vv MetaViewers) { - vv[client.NewGVR("apiextensions.k8s.io/v1/customresourcedefinitions")] = MetaViewer{ + vv[client.CrdGVR] = MetaViewer{ viewerFn: NewCRD, } } diff --git a/internal/view/restart_extender.go b/internal/view/restart_extender.go index b18550c5..0ca40297 100644 --- a/internal/view/restart_extender.go +++ b/internal/view/restart_extender.go @@ -41,7 +41,7 @@ func (r *RestartExtender) bindKeys(aa *ui.KeyActions) { )) } -func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey { +func (r *RestartExtender) restartCmd(*tcell.EventKey) *tcell.EventKey { paths := r.GetTable().GetSelectedItems() if len(paths) == 0 || paths[0] == "" { return nil @@ -53,7 +53,8 @@ func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey { if len(paths) > 1 { msg = fmt.Sprintf("Restart %d %s?", len(paths), r.GVR().R()) } - dialog.ShowConfirm(r.App().Styles.Dialog(), r.App().Content.Pages, "Confirm Restart", msg, func() { + d := r.App().Styles.Dialog() + dialog.ShowConfirm(&d, r.App().Content.Pages, "Confirm Restart", msg, func() { ctx, cancel := context.WithTimeout(context.Background(), r.App().Conn().Config().CallTimeout()) defer cancel() for _, path := range paths { diff --git a/internal/view/rs.go b/internal/view/rs.go index 1c552478..40eff8a8 100644 --- a/internal/view/rs.go +++ b/internal/view/rs.go @@ -19,7 +19,7 @@ type ReplicaSet struct { } // NewReplicaSet returns a new viewer. -func NewReplicaSet(gvr client.GVR) ResourceViewer { +func NewReplicaSet(gvr *client.GVR) ResourceViewer { r := ReplicaSet{ ResourceViewer: NewOwnerExtender( NewVulnerabilityExtender( @@ -42,7 +42,7 @@ func (r *ReplicaSet) bindKeys(aa *ui.KeyActions) { }) } -func (r *ReplicaSet) showPods(app *App, _ ui.Tabular, _ client.GVR, path string) { +func (*ReplicaSet) showPods(app *App, _ ui.Tabular, _ *client.GVR, path string) { var drs dao.ReplicaSet rs, err := drs.Load(app.factory, path) if err != nil { @@ -61,7 +61,8 @@ func (r *ReplicaSet) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey { msg := fmt.Sprintf("Rollback %s %s?", r.GVR(), path) - dialog.ShowConfirm(r.App().Styles.Dialog(), r.App().Content.Pages, "Rollback", msg, func() { + d := r.App().Styles.Dialog() + dialog.ShowConfirm(&d, r.App().Content.Pages, "Rollback", msg, func() { r.App().Flash().Infof("Rolling back %s %s", r.GVR(), path) var drs dao.ReplicaSet drs.Init(r.App().factory, r.GVR()) diff --git a/internal/view/sa.go b/internal/view/sa.go index 358a0240..68ea71be 100644 --- a/internal/view/sa.go +++ b/internal/view/sa.go @@ -19,7 +19,7 @@ type ServiceAccount struct { } // NewServiceAccount returns a new viewer. -func NewServiceAccount(gvr client.GVR) ResourceViewer { +func NewServiceAccount(gvr *client.GVR) ResourceViewer { s := ServiceAccount{ ResourceViewer: NewOwnerExtender(NewBrowser(gvr)), } @@ -36,12 +36,12 @@ func (s *ServiceAccount) bindKeys(aa *ui.KeyActions) { }) } -func (s *ServiceAccount) subjectCtx(ctx context.Context) context.Context { +func (*ServiceAccount) subjectCtx(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeySubjectKind, sa) } func (s *ServiceAccount) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanSARefs(evt, s.App(), s.GetTable(), dao.SaGVR) + return scanSARefs(evt, s.App(), s.GetTable(), client.SaGVR) } func (s *ServiceAccount) policyCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -56,7 +56,7 @@ func (s *ServiceAccount) policyCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func scanSARefs(evt *tcell.EventKey, a *App, t *Table, gvr client.GVR) *tcell.EventKey { +func scanSARefs(evt *tcell.EventKey, a *App, t *Table, gvr *client.GVR) *tcell.EventKey { path := t.GetSelectedItem() if path == "" { return evt @@ -73,7 +73,7 @@ func scanSARefs(evt *tcell.EventKey, a *App, t *Table, gvr client.GVR) *tcell.Ev return nil } a.Flash().Infof("Viewing references for %s::%s", gvr, path) - view := NewReference(client.NewGVR("references")) + view := NewReference(client.RefGVR) view.SetContextFn(refContext(gvr, path, false)) if err := a.inject(view, false); err != nil { a.Flash().Err(err) diff --git a/internal/view/scale_extender.go b/internal/view/scale_extender.go index 008bc5bb..04ad9d54 100644 --- a/internal/view/scale_extender.go +++ b/internal/view/scale_extender.go @@ -54,7 +54,7 @@ func (s *ScaleExtender) bindKeys(aa *ui.KeyActions) { } } -func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey { +func (s *ScaleExtender) scaleCmd(*tcell.EventKey) *tcell.EventKey { paths := s.GetTable().GetSelectedItems() if len(paths) == 0 { return nil @@ -137,7 +137,7 @@ func (s *ScaleExtender) makeScaleForm(fqns []string) (*tview.Form, error) { // read the replicas directly from the CRD. if meta, _ := dao.MetaAccess.MetaFor(s.GVR()); dao.IsScalable(meta) { replicas, err := s.replicasFromScaleSubresource(fqns[0]) - if err == nil && len(replicas) != 0 { + if err == nil && replicas != "" { factor = replicas } } @@ -163,7 +163,7 @@ func (s *ScaleExtender) makeScaleForm(fqns []string) (*tview.Form, error) { SetLabelColor(styles.LabelFgColor.Color()). SetFieldTextColor(styles.FieldFgColor.Color()) - f.AddInputField("Replicas:", factor, 4, func(textToCheck string, lastChar rune) bool { + f.AddInputField("Replicas:", factor, 4, func(textToCheck string, _ rune) bool { _, err := strconv.Atoi(textToCheck) return err == nil }, func(changed string) { @@ -180,7 +180,7 @@ func (s *ScaleExtender) makeScaleForm(fqns []string) (*tview.Form, error) { ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout()) defer cancel() for _, fqn := range fqns { - if err := s.scale(ctx, fqn, count); err != nil { + if err := s.scale(ctx, fqn, int32(count)); err != nil { slog.Error("Unable to scale resource", slogs.FQN, fqn) s.App().Flash().Err(err) return @@ -195,14 +195,14 @@ func (s *ScaleExtender) makeScaleForm(fqns []string) (*tview.Form, error) { f.AddButton("Cancel", func() { s.dismissDialog() }) - for i := 0; i < 2; i++ { + for i := range 2 { if b := f.GetButton(i); b != nil { b.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()) b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color()) } } - for i := 0; i < f.GetButtonCount(); i++ { + for i := range f.GetButtonCount() { f.GetButton(i). SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()). SetLabelColorActivated(styles.ButtonFocusFgColor.Color()) @@ -215,7 +215,7 @@ func (s *ScaleExtender) dismissDialog() { s.App().Content.RemovePage(scaleDialogKey) } -func (s *ScaleExtender) scale(ctx context.Context, path string, replicas int) error { +func (s *ScaleExtender) scale(ctx context.Context, path string, replicas int32) error { res, err := dao.AccessorFor(s.App().factory, s.GVR()) if err != nil { return err @@ -225,5 +225,5 @@ func (s *ScaleExtender) scale(ctx context.Context, path string, replicas int) er return fmt.Errorf("expecting a scalable resource for %q", s.GVR()) } - return scaler.Scale(ctx, path, int32(replicas)) + return scaler.Scale(ctx, path, replicas) } diff --git a/internal/view/screen_dump.go b/internal/view/screen_dump.go index c36b54a7..2d028ad2 100644 --- a/internal/view/screen_dump.go +++ b/internal/view/screen_dump.go @@ -21,7 +21,7 @@ type ScreenDump struct { } // NewScreenDump returns a new viewer. -func NewScreenDump(gvr client.GVR) ResourceViewer { +func NewScreenDump(gvr *client.GVR) ResourceViewer { s := ScreenDump{ ResourceViewer: NewBrowser(gvr), } @@ -45,12 +45,12 @@ func (s *ScreenDump) dirContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyDir, dir) } -func (s *ScreenDump) edit(app *App, _ ui.Tabular, _ client.GVR, path string) { +func (s *ScreenDump) edit(app *App, _ ui.Tabular, _ *client.GVR, path string) { slog.Debug("ScreenDump selection", slogs.FQN, path) s.Stop() defer s.Start() - if !edit(app, shellOpts{clear: true, args: []string{path}}) { + if !edit(app, &shellOpts{clear: true, args: []string{path}}) { app.Flash().Errf("Failed to launch editor") } } diff --git a/internal/view/screen_dump_test.go b/internal/view/screen_dump_test.go index b812435b..96af8bb2 100644 --- a/internal/view/screen_dump_test.go +++ b/internal/view/screen_dump_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestScreenDumpNew(t *testing.T) { - po := view.NewScreenDump(client.NewGVR("screendumps")) + po := view.NewScreenDump(client.SdGVR) - assert.Nil(t, po.Init(makeCtx())) + require.NoError(t, po.Init(makeCtx())) assert.Equal(t, "ScreenDumps", po.Name()) - assert.Equal(t, 5, len(po.Hints())) + assert.Len(t, po.Hints(), 5) } diff --git a/internal/view/secret.go b/internal/view/secret.go index 7f43edd5..ce3fa708 100644 --- a/internal/view/secret.go +++ b/internal/view/secret.go @@ -18,7 +18,7 @@ type Secret struct { } // NewSecret returns a new viewer. -func NewSecret(gvr client.GVR) ResourceViewer { +func NewSecret(gvr *client.GVR) ResourceViewer { s := Secret{ ResourceViewer: NewOwnerExtender(NewBrowser(gvr)), } @@ -35,7 +35,7 @@ func (s *Secret) bindKeys(aa *ui.KeyActions) { } func (s *Secret) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanRefs(evt, s.App(), s.GetTable(), dao.SecGVR) + return scanRefs(evt, s.App(), s.GetTable(), client.SecGVR) } func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -44,7 +44,7 @@ func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - o, err := s.App().factory.Get(s.GVR().String(), path, true, labels.Everything()) + o, err := s.App().factory.Get(s.GVR(), path, true, labels.Everything()) if err != nil { s.App().Flash().Err(err) return nil diff --git a/internal/view/secret_test.go b/internal/view/secret_test.go index a02972af..7e28ab62 100644 --- a/internal/view/secret_test.go +++ b/internal/view/secret_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSecretNew(t *testing.T) { - s := view.NewSecret(client.NewGVR("v1/secrets")) + s := view.NewSecret(client.SecGVR) - assert.Nil(t, s.Init(makeCtx())) + require.NoError(t, s.Init(makeCtx())) assert.Equal(t, "Secrets", s.Name()) - assert.Equal(t, 8, len(s.Hints())) + assert.Len(t, s.Hints(), 8) } diff --git a/internal/view/sts.go b/internal/view/sts.go index fa25e4d4..16dafaa2 100644 --- a/internal/view/sts.go +++ b/internal/view/sts.go @@ -18,7 +18,7 @@ type StatefulSet struct { } // NewStatefulSet returns a new viewer. -func NewStatefulSet(gvr client.GVR) ResourceViewer { +func NewStatefulSet(gvr *client.GVR) ResourceViewer { var s StatefulSet s.ResourceViewer = NewPortForwardExtender( NewVulnerabilityExtender( @@ -49,14 +49,14 @@ func (s *StatefulSet) logOptions(prev bool) (*dao.LogOptions, error) { return nil, err } - return podLogOptions(s.App(), path, prev, sts.ObjectMeta, sts.Spec.Template.Spec), nil + return podLogOptions(s.App(), path, prev, &sts.ObjectMeta, &sts.Spec.Template.Spec), nil } func (s *StatefulSet) bindKeys(aa *ui.KeyActions) { aa.Add(ui.KeyShiftR, ui.NewKeyAction("Sort Ready", s.GetTable().SortColCmd(readyCol, true), false)) } -func (s *StatefulSet) showPods(app *App, _ ui.Tabular, _ client.GVR, path string) { +func (s *StatefulSet) showPods(app *App, _ ui.Tabular, _ *client.GVR, path string) { i, err := s.getInstance(path) if err != nil { app.Flash().Err(err) diff --git a/internal/view/sts_test.go b/internal/view/sts_test.go index 44551945..119e8d02 100644 --- a/internal/view/sts_test.go +++ b/internal/view/sts_test.go @@ -9,12 +9,13 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestStatefulSetNew(t *testing.T) { - s := view.NewStatefulSet(client.NewGVR("apps/v1/statefulsets")) + s := view.NewStatefulSet(client.StsGVR) - assert.Nil(t, s.Init(makeCtx())) + require.NoError(t, s.Init(makeCtx())) assert.Equal(t, "StatefulSets", s.Name()) - assert.Equal(t, 14, len(s.Hints())) + assert.Len(t, s.Hints(), 14) } diff --git a/internal/view/svc.go b/internal/view/svc.go index c2f2459e..bb233e1f 100644 --- a/internal/view/svc.go +++ b/internal/view/svc.go @@ -33,7 +33,7 @@ type Service struct { } // NewService returns a new viewer. -func NewService(gvr client.GVR) ResourceViewer { +func NewService(gvr *client.GVR) ResourceViewer { s := Service{ ResourceViewer: NewPortForwardExtender( NewOwnerExtender( @@ -56,7 +56,7 @@ func (s *Service) bindKeys(aa *ui.KeyActions) { }) } -func (s *Service) showPods(a *App, _ ui.Tabular, _ client.GVR, path string) { +func (s *Service) showPods(a *App, _ ui.Tabular, _ *client.GVR, path string) { var res dao.Service res.Init(a.factory, s.GVR()) @@ -77,14 +77,14 @@ func (s *Service) showPods(a *App, _ ui.Tabular, _ client.GVR, path string) { showPods(a, path, toLabelsStr(svc.Spec.Selector), "") } -func (s *Service) checkSvc(svc *v1.Service) error { +func (*Service) checkSvc(svc *v1.Service) error { if svc.Spec.Type != "NodePort" && svc.Spec.Type != "LoadBalancer" { return errors.New("you must select a reachable service") } return nil } -func (s *Service) getExternalPort(svc *v1.Service) (string, error) { +func (*Service) getExternalPort(svc *v1.Service) (string, error) { if svc.Spec.Type == "LoadBalancer" { return "", nil } @@ -140,7 +140,7 @@ func (s *Service) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { s.App().Flash().Err(err) return nil } - if err := s.runBenchmark(port, cfg); err != nil { + if err := s.runBenchmark(port, &cfg); err != nil { s.App().Flash().Errf("Benchmark failed %v", err) s.App().ClearStatus(false) s.bench = nil @@ -150,7 +150,7 @@ func (s *Service) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { } // BOZO!! Refactor used by forwards. -func (s *Service) runBenchmark(port string, cfg config.BenchConfig) error { +func (s *Service) runBenchmark(port string, cfg *config.BenchConfig) error { if cfg.HTTP.Host == "" { return fmt.Errorf("invalid benchmark host %q", cfg.HTTP.Host) } @@ -209,7 +209,7 @@ func clearStatus(app *App) { } func fetchService(f dao.Factory, path string) (*v1.Service, error) { - o, err := f.Get("v1/services", path, true, labels.Everything()) + o, err := f.Get(client.SvcGVR, path, true, labels.Everything()) if err != nil { return nil, err } diff --git a/internal/view/svc_test.go b/internal/view/svc_test.go index 19978d8f..5e0f3ffc 100644 --- a/internal/view/svc_test.go +++ b/internal/view/svc_test.go @@ -10,17 +10,18 @@ import ( "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func init() { - dao.MetaAccess.RegisterMeta("dir", metav1.APIResource{ - Name: "dir", + dao.MetaAccess.RegisterMeta(client.DirGVR.String(), &metav1.APIResource{ + Name: "dirs", SingularName: "dir", Kind: "Directory", Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("v1/pods", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.PodGVR.String(), &metav1.APIResource{ Name: "pods", SingularName: "pod", Namespaced: true, @@ -28,7 +29,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("v1/namespaces", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.NsGVR.String(), &metav1.APIResource{ Name: "namespaces", SingularName: "namespace", Namespaced: true, @@ -36,7 +37,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("v1/services", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.SvcGVR.String(), &metav1.APIResource{ Name: "services", SingularName: "service", Namespaced: true, @@ -44,7 +45,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("v1/secrets", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.SecGVR.String(), &metav1.APIResource{ Name: "secrets", SingularName: "secret", Namespaced: true, @@ -52,7 +53,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("scheduling.k8s.io/v1/priorityclasses", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.PcGVR.String(), &metav1.APIResource{ Name: "priorityclasses", SingularName: "priorityclass", Namespaced: false, @@ -60,7 +61,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("v1/configmaps", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.CmGVR.String(), &metav1.APIResource{ Name: "configmaps", SingularName: "configmap", Namespaced: true, @@ -69,7 +70,7 @@ func init() { Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("references", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.RefGVR.String(), &metav1.APIResource{ Name: "references", SingularName: "reference", Namespaced: true, @@ -77,7 +78,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("aliases", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.AliGVR.String(), &metav1.APIResource{ Name: "aliases", SingularName: "alias", Namespaced: true, @@ -85,7 +86,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("containers", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.CoGVR.String(), &metav1.APIResource{ Name: "containers", SingularName: "container", Namespaced: true, @@ -93,7 +94,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("contexts", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.CtGVR.String(), &metav1.APIResource{ Name: "contexts", SingularName: "context", Namespaced: true, @@ -101,7 +102,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("subjects", metav1.APIResource{ + dao.MetaAccess.RegisterMeta("subjects", &metav1.APIResource{ Name: "subjects", SingularName: "subject", Namespaced: true, @@ -109,7 +110,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("rbac", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.RbacGVR.String(), &metav1.APIResource{ Name: "rbacs", SingularName: "rbac", Namespaced: true, @@ -117,7 +118,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("portforwards", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.PfGVR.String(), &metav1.APIResource{ Name: "portforwards", SingularName: "portforward", Namespaced: true, @@ -126,7 +127,7 @@ func init() { Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("screendumps", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.SdGVR.String(), &metav1.APIResource{ Name: "screendumps", SingularName: "screendump", Namespaced: true, @@ -134,7 +135,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("apps/v1/statefulsets", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.StsGVR.String(), &metav1.APIResource{ Name: "statefulsets", SingularName: "statefulset", Namespaced: true, @@ -142,7 +143,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("apps/v1/daemonsets", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.DsGVR.String(), &metav1.APIResource{ Name: "daemonsets", SingularName: "daemonset", Namespaced: true, @@ -150,7 +151,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("apps/v1/deployments", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.DpGVR.String(), &metav1.APIResource{ Name: "deployments", SingularName: "deployment", Namespaced: true, @@ -158,7 +159,7 @@ func init() { Verbs: []string{"get", "list", "watch", "delete"}, Categories: []string{"k9s"}, }) - dao.MetaAccess.RegisterMeta("v1/persistentvolumeclaims", metav1.APIResource{ + dao.MetaAccess.RegisterMeta(client.PvcGVR.String(), &metav1.APIResource{ Name: "persistentvolumeclaims", SingularName: "persistentvolumeclaim", Namespaced: true, @@ -169,9 +170,9 @@ func init() { } func TestServiceNew(t *testing.T) { - s := view.NewService(client.NewGVR("v1/services")) + s := view.NewService(client.SvcGVR) - assert.Nil(t, s.Init(makeCtx())) + require.NoError(t, s.Init(makeCtx())) assert.Equal(t, "Services", s.Name()) - assert.Equal(t, 12, len(s.Hints())) + assert.Len(t, s.Hints(), 12) } diff --git a/internal/view/table.go b/internal/view/table.go index f3e42679..d00c40bb 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -32,7 +32,7 @@ type Table struct { } // NewTable returns a new viewer. -func NewTable(gvr client.GVR) *Table { +func NewTable(gvr *client.GVR) *Table { t := Table{ Table: ui.NewTable(gvr), } @@ -73,7 +73,7 @@ func (t *Table) SetCommand(cmd *cmd.Interpreter) { // HeaderIndex returns index of a given column or false if not found. func (t *Table) HeaderIndex(colName string) (int, bool) { - for i := 0; i < t.GetColumnCount(); i++ { + for i := range t.GetColumnCount() { h := t.GetCell(0, i) if h == nil { continue @@ -168,7 +168,7 @@ func (t *Table) SetEnterFn(f EnterFunc) { } // SetExtraActionsFn specifies custom keyboard behavior. -func (t *Table) SetExtraActionsFn(BoostActionsFunc) {} +func (*Table) SetExtraActionsFn(BoostActionsFunc) {} // BufferCompleted indicates input was accepted. func (t *Table) BufferCompleted(text, _ string) { @@ -178,7 +178,7 @@ func (t *Table) BufferCompleted(text, _ string) { } // BufferChanged indicates the buffer was changed. -func (t *Table) BufferChanged(_, _ string) {} +func (*Table) BufferChanged(_, _ string) {} // BufferActive indicates the buff activity changed. func (t *Table) BufferActive(state bool, k model.BufferKind) { @@ -188,7 +188,7 @@ func (t *Table) BufferActive(state bool, k model.BufferKind) { } } -func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { +func (t *Table) saveCmd(*tcell.EventKey) *tcell.EventKey { if path, err := saveTable(t.app.Config.K9s.ContextScreenDumpDir(), t.GVR().R(), t.Path, t.GetFilteredData()); err != nil { t.app.Flash().Err(err) } else { @@ -213,12 +213,12 @@ func (t *Table) bindKeys() { }) } -func (t *Table) toggleFaultCmd(evt *tcell.EventKey) *tcell.EventKey { +func (t *Table) toggleFaultCmd(*tcell.EventKey) *tcell.EventKey { t.ToggleToast() return nil } -func (t *Table) toggleWideCmd(evt *tcell.EventKey) *tcell.EventKey { +func (t *Table) toggleWideCmd(*tcell.EventKey) *tcell.EventKey { t.ToggleWide() return nil } @@ -253,21 +253,21 @@ func (t *Table) cpNsCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (t *Table) markCmd(evt *tcell.EventKey) *tcell.EventKey { +func (t *Table) markCmd(*tcell.EventKey) *tcell.EventKey { t.ToggleMark() t.Refresh() return nil } -func (t *Table) markSpanCmd(evt *tcell.EventKey) *tcell.EventKey { +func (t *Table) markSpanCmd(*tcell.EventKey) *tcell.EventKey { t.SpanMark() t.Refresh() return nil } -func (t *Table) clearMarksCmd(evt *tcell.EventKey) *tcell.EventKey { +func (t *Table) clearMarksCmd(*tcell.EventKey) *tcell.EventKey { t.ClearMarks() t.Refresh() diff --git a/internal/view/table_helper.go b/internal/view/table_helper.go index ca4154a8..31efe246 100644 --- a/internal/view/table_helper.go +++ b/internal/view/table_helper.go @@ -22,7 +22,7 @@ import ( func computeFilename(dumpPath, ns, title, path string) (string, error) { now := time.Now().UnixNano() - dir := filepath.Join(dumpPath) + dir := dumpPath if err := ensureDir(dir); err != nil { return "", err } diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go index e4a3b1fa..29966e89 100644 --- a/internal/view/table_int_test.go +++ b/internal/view/table_int_test.go @@ -22,27 +22,28 @@ import ( "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) func TestTableSave(t *testing.T) { v := NewTable(client.NewGVR("test")) - assert.NoError(t, v.Init(makeContext())) + require.NoError(t, v.Init(makeContext())) v.SetTitle("k9s-test") - assert.NoError(t, ensureDumpDir("/tmp/test-dumps")) + require.NoError(t, ensureDumpDir("/tmp/test-dumps")) dir := v.app.Config.K9s.ContextScreenDumpDir() c1, _ := os.ReadDir(dir) v.saveCmd(nil) c2, _ := os.ReadDir(dir) - assert.Equal(t, len(c2), len(c1)+1) + assert.Len(t, c2, len(c1)+1) } func TestTableNew(t *testing.T) { v := NewTable(client.NewGVR("test")) - assert.NoError(t, v.Init(makeContext())) + require.NoError(t, v.Init(makeContext())) data := model1.NewTableDataWithRows( client.NewGVR("test"), @@ -73,7 +74,7 @@ func TestTableNew(t *testing.T) { func TestTableViewFilter(t *testing.T) { v := NewTable(client.NewGVR("test")) - assert.NoError(t, v.Init(makeContext())) + require.NoError(t, v.Init(makeContext())) v.SetModel(&mockTableModel{}) v.Refresh() @@ -85,7 +86,7 @@ func TestTableViewFilter(t *testing.T) { func TestTableViewSort(t *testing.T) { v := NewTable(client.NewGVR("test")) - assert.NoError(t, v.Init(makeContext())) + require.NoError(t, v.Init(makeContext())) v.SetModel(new(mockTableModel)) uu := map[string]struct { @@ -113,12 +114,12 @@ func TestTableViewSort(t *testing.T) { for k := range uu { u := uu[k] v.SortColCmd(u.sortCol, true)(nil) - assert.Equal(t, len(u.sorted)+1, v.GetRowCount()) + assert.Len(t, u.sorted, v.GetRowCount()-1) for i, s := range u.sorted { assert.Equal(t, s, v.GetCell(i+1, 0).Text) } v.SortInvertCmd(nil) - assert.Equal(t, len(u.reversed)+1, v.GetRowCount()) + assert.Len(t, u.reversed, v.GetRowCount()-1) for i, s := range u.reversed { assert.Equal(t, s, v.GetCell(i+1, 0).Text) } @@ -132,40 +133,36 @@ type mockTableModel struct{} var _ ui.Tabular = (*mockTableModel)(nil) -func (t *mockTableModel) SetViewSetting(context.Context, *config.ViewSetting) {} -func (t *mockTableModel) SetInstance(string) {} -func (t *mockTableModel) SetLabelFilter(string) {} -func (t *mockTableModel) GetLabelFilter() string { return "" } -func (t *mockTableModel) Empty() bool { return false } -func (t *mockTableModel) RowCount() int { return 1 } -func (t *mockTableModel) HasMetrics() bool { return true } -func (t *mockTableModel) Peek() *model1.TableData { return makeTableData() } -func (t *mockTableModel) Refresh(context.Context) error { return nil } -func (t *mockTableModel) ClusterWide() bool { return false } -func (t *mockTableModel) GetNamespace() string { return "blee" } -func (t *mockTableModel) SetNamespace(string) {} -func (t *mockTableModel) ToggleToast() {} -func (t *mockTableModel) AddListener(model.TableListener) {} -func (t *mockTableModel) RemoveListener(model.TableListener) {} -func (t *mockTableModel) Watch(context.Context) error { return nil } -func (t *mockTableModel) Get(context.Context, string) (runtime.Object, error) { +func (*mockTableModel) SetViewSetting(context.Context, *config.ViewSetting) {} +func (*mockTableModel) SetInstance(string) {} +func (*mockTableModel) SetLabelFilter(string) {} +func (*mockTableModel) GetLabelFilter() string { return "" } +func (*mockTableModel) Empty() bool { return false } +func (*mockTableModel) RowCount() int { return 1 } +func (*mockTableModel) HasMetrics() bool { return true } +func (*mockTableModel) Peek() *model1.TableData { return makeTableData() } +func (*mockTableModel) Refresh(context.Context) error { return nil } +func (*mockTableModel) ClusterWide() bool { return false } +func (*mockTableModel) GetNamespace() string { return "blee" } +func (*mockTableModel) SetNamespace(string) {} +func (*mockTableModel) ToggleToast() {} +func (*mockTableModel) AddListener(model.TableListener) {} +func (*mockTableModel) RemoveListener(model.TableListener) {} +func (*mockTableModel) Watch(context.Context) error { return nil } +func (*mockTableModel) Get(context.Context, string) (runtime.Object, error) { return nil, nil } - -func (t *mockTableModel) Delete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error { +func (*mockTableModel) Delete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error { return nil } - -func (t *mockTableModel) Describe(context.Context, string) (string, error) { +func (*mockTableModel) Describe(context.Context, string) (string, error) { return "", nil } - -func (t *mockTableModel) ToYAML(ctx context.Context, path string) (string, error) { +func (*mockTableModel) ToYAML(context.Context, string) (string, error) { return "", nil } - -func (t *mockTableModel) InNamespace(string) bool { return true } -func (t *mockTableModel) SetRefreshRate(time.Duration) {} +func (*mockTableModel) InNamespace(string) bool { return true } +func (*mockTableModel) SetRefreshRate(time.Duration) {} func makeTableData() *model1.TableData { return model1.NewTableDataWithRows( diff --git a/internal/view/types.go b/internal/view/types.go index 46485ba0..1320e46d 100644 --- a/internal/view/types.go +++ b/internal/view/types.go @@ -32,7 +32,7 @@ type ( BoostActionsFunc func(ui.KeyActions) // EnterFunc represents an enter key action. - EnterFunc func(app *App, model ui.Tabular, gvr client.GVR, path string) + EnterFunc func(app *App, model ui.Tabular, gvr *client.GVR, path string) // LogOptionsFunc returns the active log options. LogOptionsFunc func(bool) (*dao.LogOptions, error) @@ -86,7 +86,7 @@ type ResourceViewer interface { SetEnvFn(EnvFunc) // GVR returns a resource descriptor. - GVR() client.GVR + GVR() *client.GVR // SetContextFn provision a custom context. SetContextFn(ContextFunc) @@ -127,7 +127,7 @@ type SubjectViewer interface { } // ViewerFunc returns a viewer matching a given gvr. -type ViewerFunc func(client.GVR) ResourceViewer +type ViewerFunc func(*client.GVR) ResourceViewer // MetaViewer represents a registered meta viewer. type MetaViewer struct { @@ -136,4 +136,4 @@ type MetaViewer struct { } // MetaViewers represents a collection of meta viewers. -type MetaViewers map[client.GVR]MetaViewer +type MetaViewers map[*client.GVR]MetaViewer diff --git a/internal/view/user.go b/internal/view/user.go index 06850451..65000315 100644 --- a/internal/view/user.go +++ b/internal/view/user.go @@ -18,7 +18,7 @@ type User struct { } // NewUser returns a new subject viewer. -func NewUser(gvr client.GVR) ResourceViewer { +func NewUser(gvr *client.GVR) ResourceViewer { u := User{ResourceViewer: NewBrowser(gvr)} u.AddBindKeysFn(u.bindKeys) u.SetContextFn(u.subjectCtx) @@ -34,7 +34,7 @@ func (u *User) bindKeys(aa *ui.KeyActions) { }) } -func (u *User) subjectCtx(ctx context.Context) context.Context { +func (*User) subjectCtx(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeySubjectKind, "User") } diff --git a/internal/view/value_extender.go b/internal/view/value_extender.go index d772454b..950486b0 100644 --- a/internal/view/value_extender.go +++ b/internal/view/value_extender.go @@ -24,7 +24,7 @@ type ValueExtender struct { func NewValueExtender(r ResourceViewer) ResourceViewer { p := ValueExtender{ResourceViewer: r} p.AddBindKeysFn(p.bindKeys) - p.GetTable().SetEnterFn(func(app *App, model ui.Tabular, gvr client.GVR, path string) { + p.GetTable().SetEnterFn(func(*App, ui.Tabular, *client.GVR, string) { p.valuesCmd(nil) }) @@ -49,13 +49,13 @@ func (v *ValueExtender) defaultCtx() context.Context { return context.WithValue(context.Background(), internal.KeyFactory, v.App().factory) } -func showValues(ctx context.Context, app *App, path string, gvr client.GVR) { +func showValues(ctx context.Context, app *App, path string, gvr *client.GVR) { vm := model.NewValues(gvr, path) if err := vm.Init(app.factory); err != nil { app.Flash().Errf("Initializing the values model failed: %s", err) } - toggleValuesCmd := func(evt *tcell.EventKey) *tcell.EventKey { + toggleValuesCmd := func(*tcell.EventKey) *tcell.EventKey { if err := vm.ToggleValues(); err != nil { app.Flash().Errf("Values toggle failed: %s", err) return nil diff --git a/internal/view/vul_extender.go b/internal/view/vul_extender.go index ebc373b6..fa6f3ca8 100644 --- a/internal/view/vul_extender.go +++ b/internal/view/vul_extender.go @@ -34,8 +34,8 @@ func (v *VulnerabilityExtender) bindKeys(aa *ui.KeyActions) { } } -func (v *VulnerabilityExtender) showVulCmd(evt *tcell.EventKey) *tcell.EventKey { - isv := NewImageScan(client.NewGVR("scans")) +func (v *VulnerabilityExtender) showVulCmd(*tcell.EventKey) *tcell.EventKey { + isv := NewImageScan(client.ScGVR) isv.SetContextFn(v.selContext) if err := v.App().inject(isv, false); err != nil { v.App().Flash().Err(err) diff --git a/internal/view/workload.go b/internal/view/workload.go index cf0beedd..72aaf727 100644 --- a/internal/view/workload.go +++ b/internal/view/workload.go @@ -26,7 +26,7 @@ type Workload struct { } // NewWorkload returns a new viewer. -func NewWorkload(gvr client.GVR) ResourceViewer { +func NewWorkload(gvr *client.GVR) ResourceViewer { w := Workload{ ResourceViewer: NewBrowser(gvr), } @@ -67,17 +67,17 @@ func (w *Workload) bindKeys(aa *ui.KeyActions) { }) } -func parsePath(path string) (client.GVR, string, bool) { +func parsePath(path string) (*client.GVR, string, bool) { tt := strings.Split(path, "|") if len(tt) != 3 { slog.Error("Unable to parse workload path", slogs.Path, path) - return client.NewGVR(""), client.FQN("", ""), false + return client.NoGVR, client.FQN("", ""), false } return client.NewGVR(tt[0]), client.FQN(tt[1], tt[2]), true } -func (w *Workload) showRes(app *App, _ ui.Tabular, _ client.GVR, path string) { +func (*Workload) showRes(app *App, _ ui.Tabular, _ *client.GVR, path string) { gvr, fqn, ok := parsePath(path) if !ok { app.Flash().Err(fmt.Errorf("unable to parse path: %q", path)) @@ -105,7 +105,7 @@ func (w *Workload) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (w *Workload) defaultContext(gvr client.GVR, fqn string) context.Context { +func (w *Workload) defaultContext(gvr *client.GVR, fqn string) context.Context { ctx := context.WithValue(context.Background(), internal.KeyFactory, w.App().factory) ctx = context.WithValue(ctx, internal.KeyGVR, gvr) if fqn != "" { @@ -148,7 +148,8 @@ func (w *Workload) resourceDelete(selections []string, msg string) { } w.GetTable().Start() } - dialog.ShowDelete(w.App().Styles.Dialog(), w.App().Content.Pages, msg, okFn, func() {}) + d := w.App().Styles.Dialog() + dialog.ShowDelete(&d, w.App().Content.Pages, msg, okFn, func() {}) } func (w *Workload) describeCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/xray.go b/internal/view/xray.go index f46edc06..c97c5733 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -28,6 +28,7 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" ) const xrayTitle = "Xray" @@ -39,15 +40,15 @@ type Xray struct { *ui.Tree app *App - gvr client.GVR - meta metav1.APIResource + gvr *client.GVR + meta *metav1.APIResource model *model.Tree cancelFn context.CancelFunc envFn EnvFunc } // NewXray returns a new view. -func NewXray(gvr client.GVR) ResourceViewer { +func NewXray(gvr *client.GVR) ResourceViewer { return &Xray{ gvr: gvr, Tree: ui.NewTree(), @@ -55,9 +56,9 @@ func NewXray(gvr client.GVR) ResourceViewer { } } -func (x *Xray) SetCommand(*cmd.Interpreter) {} -func (x *Xray) SetFilter(string) {} -func (x *Xray) SetLabelFilter(map[string]string) {} +func (*Xray) SetCommand(*cmd.Interpreter) {} +func (*Xray) SetFilter(string) {} +func (*Xray) SetLabelFilter(map[string]string) {} // Init initializes the view. func (x *Xray) Init(ctx context.Context) error { @@ -117,7 +118,7 @@ func (x *Xray) ExtraHints() map[string]string { } // SetInstance sets specific resource instance. -func (x *Xray) SetInstance(string) {} +func (*Xray) SetInstance(string) {} func (x *Xray) bindKeys() { x.Actions().Bulk(ui.KeyMap{ @@ -158,7 +159,7 @@ func (x *Xray) refreshActions() { gvr := spec.GVR() var err error - x.meta, err = dao.MetaAccess.MetaFor(client.NewGVR(gvr)) + x.meta, err = dao.MetaAccess.MetaFor(gvr) if err != nil { slog.Warn("No meta found!", slogs.GVR, gvr, @@ -181,16 +182,16 @@ func (x *Xray) refreshActions() { } switch gvr { - case "v1/namespaces": + case client.NsGVR: x.Actions().Delete(tcell.KeyEnter) - case "containers": + case client.CoGVR: x.Actions().Delete(tcell.KeyEnter) aa.Bulk(ui.KeyMap{ ui.KeyS: ui.NewKeyAction("Shell", x.shellCmd, true), ui.KeyL: ui.NewKeyAction("Logs", x.logsCmd(false), true), ui.KeyP: ui.NewKeyAction("Logs Previous", x.logsCmd(true), true), }) - case "v1/pods": + case client.PodGVR: aa.Bulk(ui.KeyMap{ ui.KeyS: ui.NewKeyAction("Shell", x.shellCmd, true), ui.KeyA: ui.NewKeyAction("Attach", x.attachCmd, true), @@ -248,7 +249,7 @@ func (x *Xray) k9sEnv() Env { } switch spec.GVR() { - case "containers": + case client.CoGVR: _, co := client.Namespaced(spec.Path()) env["CONTAINER"] = co ns, n := client.Namespaced(*spec.ParentPath()) @@ -262,12 +263,12 @@ func (x *Xray) k9sEnv() Env { } // Aliases returns all available aliases. -func (x *Xray) Aliases() map[string]struct{} { - return aliasesFor(x.meta, x.app.command.AliasesFor(x.meta.Name)) +func (x *Xray) Aliases() sets.Set[string] { + return aliases(x.meta, x.app.command.AliasesFor(client.NewGVRFromMeta(x.meta))) } func (x *Xray) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { + return func(*tcell.EventKey) *tcell.EventKey { spec := x.selectedSpec() if spec == nil { return nil @@ -282,13 +283,13 @@ func (x *Xray) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey { func (x *Xray) showLogs(spec *xray.NodeSpec, prev bool) { // Need to load and wait for pods path, co := spec.Path(), "" - if spec.GVR() == "containers" { + if spec.GVR() == client.CoGVR { _, coName := client.Namespaced(spec.Path()) path, co = *spec.ParentPath(), coName } ns, _ := client.Namespaced(path) - _, err := x.app.factory.CanForResource(ns, "v1/pods", client.ListAccess) + _, err := x.app.factory.CanForResource(ns, client.PodGVR, client.ListAccess) if err != nil { x.app.Flash().Err(err) return @@ -299,12 +300,12 @@ func (x *Xray) showLogs(spec *xray.NodeSpec, prev bool) { Container: co, Previous: prev, } - if err := x.app.inject(NewLog(client.NewGVR("v1/pods"), &opts), false); err != nil { + if err := x.app.inject(NewLog(client.PodGVR, &opts), false); err != nil { x.app.Flash().Err(err) } } -func (x *Xray) shellCmd(evt *tcell.EventKey) *tcell.EventKey { +func (x *Xray) shellCmd(*tcell.EventKey) *tcell.EventKey { spec := x.selectedSpec() if spec == nil { return nil @@ -316,7 +317,7 @@ func (x *Xray) shellCmd(evt *tcell.EventKey) *tcell.EventKey { } path, co := spec.Path(), "" - if spec.GVR() == "containers" { + if spec.GVR() == client.CoGVR { _, co = client.Namespaced(spec.Path()) path = *spec.ParentPath() } @@ -328,7 +329,7 @@ func (x *Xray) shellCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (x *Xray) attachCmd(evt *tcell.EventKey) *tcell.EventKey { +func (x *Xray) attachCmd(*tcell.EventKey) *tcell.EventKey { spec := x.selectedSpec() if spec == nil { return nil @@ -340,7 +341,7 @@ func (x *Xray) attachCmd(evt *tcell.EventKey) *tcell.EventKey { } path, co := spec.Path(), "" - if spec.GVR() == "containers" { + if spec.GVR() == client.CoGVR { path = *spec.ParentPath() } @@ -381,8 +382,7 @@ func (x *Xray) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { x.Stop() defer x.Start() { - gvr := client.NewGVR(spec.GVR()) - meta, err := dao.MetaAccess.MetaFor(gvr) + meta, err := dao.MetaAccess.MetaFor(spec.GVR()) if err != nil { slog.Warn("No meta found!", slogs.GVR, spec.GVR(), @@ -390,7 +390,7 @@ func (x *Xray) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { ) return nil } - x.resourceDelete(gvr, spec, fmt.Sprintf("Delete %s %s?", meta.SingularName, spec.Path())) + x.resourceDelete(spec.GVR(), spec, fmt.Sprintf("Delete %s %s?", meta.SingularName, spec.Path())) } return nil @@ -407,7 +407,7 @@ func (x *Xray) describeCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (x *Xray) describe(gvr, path string) { +func (x *Xray) describe(gvr *client.GVR, path string) { ctx := context.Background() ctx = context.WithValue(ctx, internal.KeyFactory, x.app.factory) @@ -434,14 +434,16 @@ func (x *Xray) editCmd(evt *tcell.EventKey) *tcell.EventKey { { ns, n := client.Namespaced(spec.Path()) args := make([]string, 0, 10) - args = append(args, "edit") - args = append(args, client.NewGVR(spec.GVR()).R()) - args = append(args, "-n", ns) - args = append(args, "--context", x.app.Config.K9s.ActiveContextName()) + args = append(args, + "edit", + spec.GVR().R(), + "-n", ns, + "--context", x.app.Config.K9s.ActiveContextName(), + ) if cfg := x.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { args = append(args, "--kubeconfig", *cfg) } - if err := runK(x.app, shellOpts{args: append(args, n)}); err != nil { + if err := runK(x.app, &shellOpts{args: append(args, n)}); err != nil { x.app.Flash().Errf("Edit exec failed: %s", err) } } @@ -470,7 +472,7 @@ func (x *Xray) resetCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { +func (x *Xray) gotoCmd(*tcell.EventKey) *tcell.EventKey { if x.CmdBuff().IsActive() { if internal.IsLabelSelector(x.CmdBuff().GetText()) { x.Start() @@ -488,7 +490,7 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { if len(strings.Split(spec.Path(), "/")) == 1 { return nil } - x.app.gotoResource(client.NewGVR(spec.GVR()).R(), spec.Path(), false, true) + x.app.gotoResource(spec.GVR().String(), spec.Path(), false, true) return nil } @@ -571,7 +573,7 @@ func (x *Xray) update(node *xray.TreeNode) { // TreeChanged notifies the model data changed. func (x *Xray) TreeChanged(node *xray.TreeNode) { - x.Count = node.Count(x.gvr.String()) + x.Count = node.Count(x.gvr) x.update(x.filter(node)) x.UpdateTitle() } @@ -585,10 +587,10 @@ func (x *Xray) hydrate(parent *tview.TreeNode, n *xray.TreeNode) { } // SetEnvFn sets the custom environment function. -func (x *Xray) SetEnvFn(EnvFunc) {} +func (*Xray) SetEnvFn(EnvFunc) {} // Refresh updates the view. -func (x *Xray) Refresh() {} +func (*Xray) Refresh() {} // BufferCompleted indicates the buffer was changed. func (x *Xray) BufferCompleted(_, _ string) { @@ -596,7 +598,7 @@ func (x *Xray) BufferCompleted(_, _ string) { } // BufferChanged indicates the buffer was changed. -func (x *Xray) BufferChanged(_, _ string) {} +func (*Xray) BufferChanged(_, _ string) {} // BufferActive indicates the buff activity changed. func (x *Xray) BufferActive(state bool, k model.BufferKind) { @@ -637,19 +639,19 @@ func (x *Xray) Stop() { } // AddBindKeysFn sets up extra key bindings. -func (x *Xray) AddBindKeysFn(BindKeysFunc) {} +func (*Xray) AddBindKeysFn(BindKeysFunc) {} // SetContextFn sets custom context. -func (x *Xray) SetContextFn(ContextFunc) {} +func (*Xray) SetContextFn(ContextFunc) {} // Name returns the component name. -func (x *Xray) Name() string { return "XRay" } +func (*Xray) Name() string { return "XRay" } // GetTable returns the underlying table. -func (x *Xray) GetTable() *Table { return nil } +func (*Xray) GetTable() *Table { return nil } // GVR returns a resource descriptor. -func (x *Xray) GVR() client.GVR { return x.gvr } +func (x *Xray) GVR() *client.GVR { return x.gvr } // App returns the current app handle. func (x *Xray) App() *App { @@ -671,14 +673,14 @@ func (x *Xray) styleTitle() string { ns = client.NamespaceAll } - var title string + var ( + title string + styles = x.app.Styles.Frame() + ) if ns == client.ClusterScope { - title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, render.AsThousands(int64(x.Count))), x.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, render.AsThousands(int64(x.Count))), &styles) } else { - title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, render.AsThousands(int64(x.Count))), x.app.Styles.Frame()) - } - if ic := ui.ROIndicator(x.app.Config.IsReadOnly(), x.app.Config.K9s.UI.NoIcons); ic != "" { - title = " " + ic + title + title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, render.AsThousands(int64(x.Count))), &styles) } buff := x.CmdBuff().GetText() @@ -689,11 +691,12 @@ func (x *Xray) styleTitle() string { buff = ui.TrimLabelSelector(buff) } - return title + ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), x.app.Styles.Frame()) + return title + ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), &styles) } -func (x *Xray) resourceDelete(gvr client.GVR, spec *xray.NodeSpec, msg string) { - dialog.ShowDelete(x.app.Styles.Dialog(), x.app.Content.Pages, msg, func(propagation *metav1.DeletionPropagation, force bool) { +func (x *Xray) resourceDelete(gvr *client.GVR, spec *xray.NodeSpec, msg string) { + d := x.app.Styles.Dialog() + dialog.ShowDelete(&d, x.app.Content.Pages, msg, func(_ *metav1.DeletionPropagation, force bool) { x.app.Flash().Infof("Delete resource %s %s", spec.GVR(), spec.Path()) accessor, err := dao.AccessorFor(x.app.factory, gvr) if err != nil { @@ -758,7 +761,7 @@ func rxInverseFilter(q, path string) bool { return true } -func makeTreeNode(node *xray.TreeNode, expanded bool, showIcons bool, styles *config.Styles) *tview.TreeNode { +func makeTreeNode(node *xray.TreeNode, expanded, showIcons bool, styles *config.Styles) *tview.TreeNode { n := tview.NewTreeNode("No data...") if node != nil { n.SetText(node.Title(showIcons)) diff --git a/internal/view/yaml.go b/internal/view/yaml.go index 37a9032a..30e13ed3 100644 --- a/internal/view/yaml.go +++ b/internal/view/yaml.go @@ -19,8 +19,8 @@ import ( ) var ( - keyValRX = regexp.MustCompile(`\A(\s*)([\w|\-|\.|\/|\s]+):\s(.+)\z`) - keyRX = regexp.MustCompile(`\A(\s*)([\w|\-|\.|\/|\s]+):\s*\z`) + keyValRX = regexp.MustCompile(`\A(\s*)([\w\-./\s]+):\s(.+)\z`) + keyRX = regexp.MustCompile(`\A(\s*)([\w\-./\s]+):\s*\z`) searchRX = regexp.MustCompile(`<<<("search_\d+")>>>(.+)<<<"">>>`) ) @@ -93,7 +93,7 @@ func saveYAML(dir, name, raw string) (string, error) { ) } }() - if _, err := file.Write([]byte(raw)); err != nil { + if _, err := file.WriteString(raw); err != nil { return "", err } diff --git a/internal/vul/scanner.go b/internal/vul/scanner.go index baa8f12f..e235d46b 100644 --- a/internal/vul/scanner.go +++ b/internal/vul/scanner.go @@ -29,7 +29,6 @@ import ( "github.com/anchore/syft/syft" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/slogs" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var ImgScanner *imageScanner @@ -60,8 +59,8 @@ func NewImageScanner(cfg config.ImageScans, l *slog.Logger) *imageScanner { } } -func (s *imageScanner) ShouldExcludes(m metav1.ObjectMeta) bool { - return s.config.ShouldExclude(m.Namespace, m.Labels) +func (s *imageScanner) ShouldExcludes(ns string, lbls map[string]string) bool { + return s.config.ShouldExclude(ns, lbls) } // GetScan fetch scan for a given image. Returns ok=false when not found. @@ -100,8 +99,8 @@ func (s *imageScanner) Init(name, version string) { return } - if err := validateDBLoad(err, s.dbStatus); err != nil { - s.log.Error("VulDb validate failed", slogs.Error, err) + if e := validateDBLoad(err, s.dbStatus); e != nil { + s.log.Error("VulDb validate failed", slogs.Error, e) return } @@ -168,7 +167,7 @@ func (s *imageScanner) scanWorker(ctx context.Context, img string) { func (s *imageScanner) scan(_ context.Context, img string, sc *Scan) error { defer func(t time.Time) { - s.log.Debug("Time to run vulscan", + s.log.Debug("[Vulscan] perf", slogs.Image, img, slogs.Elapsed, time.Since(t), ) diff --git a/internal/vul/table_test.go b/internal/vul/table_test.go index b8e5b2f8..115f14fa 100644 --- a/internal/vul/table_test.go +++ b/internal/vul/table_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_sort(t *testing.T) { @@ -73,14 +74,14 @@ func makeTable(t *testing.T, path string) *table { defer func() { _ = f.Close() }() - assert.NoError(t, err) + require.NoError(t, err) sc := bufio.NewScanner(f) var tt table for sc.Scan() { ff := strings.Fields(sc.Text()) tt.addRow(newRow(ff...)) } - assert.NoError(t, sc.Err()) + require.NoError(t, sc.Err()) return &tt } diff --git a/internal/watch/factory.go b/internal/watch/factory.go index d7a1d279..17c9382a 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -72,7 +72,7 @@ func (f *Factory) Terminate() { } // List returns a resource collection. -func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]runtime.Object, error) { +func (f *Factory) List(gvr *client.GVR, ns string, wait bool, labels labels.Selector) ([]runtime.Object, error) { if client.IsAllNamespace(ns) { ns = client.BlankNamespace } @@ -99,7 +99,7 @@ func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]run } // HasSynced checks if given informer is up to date. -func (f *Factory) HasSynced(gvr, ns string) (bool, error) { +func (f *Factory) HasSynced(gvr *client.GVR, ns string) (bool, error) { inf, err := f.CanForResource(ns, gvr, client.ListAccess) if err != nil { return false, err @@ -109,7 +109,7 @@ func (f *Factory) HasSynced(gvr, ns string) (bool, error) { } // Get retrieves a given resource. -func (f *Factory) Get(gvr, fqn string, wait bool, sel labels.Selector) (runtime.Object, error) { +func (f *Factory) Get(gvr *client.GVR, fqn string, wait bool, _ labels.Selector) (runtime.Object, error) { ns, n := namespaced(fqn) if client.IsAllNamespace(ns) { ns = client.BlankNamespace @@ -200,7 +200,7 @@ func (f *Factory) isClusterWide() bool { } // CanForResource return an informer is user has access. -func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { +func (f *Factory) CanForResource(ns string, gvr *client.GVR, verbs []string) (informers.GenericInformer, error) { auth, err := f.Client().CanI(ns, gvr, "", verbs) if err != nil { return nil, err @@ -213,12 +213,12 @@ func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.Gene } // ForResource returns an informer for a given resource. -func (f *Factory) ForResource(ns, gvr string) (informers.GenericInformer, error) { +func (f *Factory) ForResource(ns string, gvr *client.GVR) (informers.GenericInformer, error) { fact, err := f.ensureFactory(ns) if err != nil { return nil, err } - inf := fact.ForResource(toGVR(gvr)) + inf := fact.ForResource(gvr.GVR()) if inf == nil { slog.Error("No informer found", slogs.GVR, gvr, @@ -306,7 +306,7 @@ func (f *Factory) ValidatePortForwards() { if len(paths) < 1 { slog.Error("Invalid port-forward path", slogs.Path, tokens[0]) } - o, err := f.Get("v1/pods", paths[0], false, labels.Everything()) + o, err := f.Get(client.PodGVR, paths[0], false, labels.Everything()) if err != nil { fwd.Stop() delete(f.forwarders, k) diff --git a/internal/watch/forwarders_test.go b/internal/watch/forwarders_test.go index 4ebbff7d..79e466c8 100644 --- a/internal/watch/forwarders_test.go +++ b/internal/watch/forwarders_test.go @@ -173,7 +173,7 @@ func newNoOpForwarder() noOpForwarder { return noOpForwarder{} } -func (noOpForwarder) Start(path string, tunnel port.PortTunnel) (*portforward.PortForwarder, error) { +func (noOpForwarder) Start(string, port.PortTunnel) (*portforward.PortForwarder, error) { return nil, nil } func (noOpForwarder) Stop() {} diff --git a/internal/watch/helper.go b/internal/watch/helper.go index 67e79455..fd140377 100644 --- a/internal/watch/helper.go +++ b/internal/watch/helper.go @@ -25,10 +25,10 @@ func toGVR(gvr string) schema.GroupVersionResource { } } -func namespaced(n string) (string, string) { - ns, po := path.Split(n) +func namespaced(n string) (ns, res string) { + ns, res = path.Split(n) - return strings.Trim(ns, "/"), po + return strings.Trim(ns, "/"), res } // DumpFactory for debug. @@ -41,7 +41,7 @@ func DumpFactory(f *Factory) { } // DebugFactory for debug. -func DebugFactory(f *Factory, ns string, gvr string) { +func DebugFactory(f *Factory, ns, gvr string) { slog.Debug(fmt.Sprintf("----------- DEBUG FACTORY (%s) -------------", gvr)) fac, ok := f.factories[ns] if !ok { diff --git a/internal/xray/container.go b/internal/xray/container.go index 1a0f2136..09a58cb2 100644 --- a/internal/xray/container.go +++ b/internal/xray/container.go @@ -21,7 +21,7 @@ import ( type Container struct{} // Render renders an xray node. -func (c *Container) Render(ctx context.Context, ns string, o interface{}) error { +func (c *Container) Render(ctx context.Context, ns string, o any) error { co, ok := o.(render.ContainerRes) if !ok { return fmt.Errorf("expected ContainerRes, but got %T", o) @@ -32,7 +32,7 @@ func (c *Container) Render(ctx context.Context, ns string, o interface{}) error return fmt.Errorf("no factory found in context") } - root := NewTreeNode("containers", client.FQN(ns, co.Container.Name)) + root := NewTreeNode(client.CoGVR, client.FQN(ns, co.Container.Name)) parent, ok := ctx.Value(KeyParent).(*TreeNode) if !ok { return fmt.Errorf("expecting a TreeNode but got %T", ctx.Value(KeyParent)) @@ -55,11 +55,11 @@ func (c *Container) envRefs(f dao.Factory, parent *TreeNode, ns string, co *v1.C for _, e := range co.EnvFrom { if e.ConfigMapRef != nil { - gvr, id := "v1/configmaps", client.FQN(ns, e.ConfigMapRef.Name) + gvr, id := client.CmGVR, client.FQN(ns, e.ConfigMapRef.Name) addRef(f, parent, gvr, id, e.ConfigMapRef.Optional) } if e.SecretRef != nil { - gvr, id := "v1/secrets", client.FQN(ns, e.SecretRef.Name) + gvr, id := client.SecGVR, client.FQN(ns, e.SecretRef.Name) addRef(f, parent, gvr, id, e.SecretRef.Optional) } } @@ -69,7 +69,7 @@ func (c *Container) secretRefs(f dao.Factory, parent *TreeNode, ns string, ref * if ref == nil { return } - gvr, id := "v1/secrets", client.FQN(ns, ref.Name) + gvr, id := client.SecGVR, client.FQN(ns, ref.Name) addRef(f, parent, gvr, id, ref.Optional) } @@ -77,14 +77,14 @@ func (c *Container) configMapRefs(f dao.Factory, parent *TreeNode, ns string, re if ref == nil { return } - gvr, id := "v1/configmaps", client.FQN(ns, ref.Name) + gvr, id := client.CmGVR, client.FQN(ns, ref.Name) addRef(f, parent, gvr, id, ref.Optional) } // ---------------------------------------------------------------------------- // Helpers... -func addRef(f dao.Factory, parent *TreeNode, gvr, id string, optional *bool) { +func addRef(f dao.Factory, parent *TreeNode, gvr *client.GVR, id string, optional *bool) { if parent.Find(gvr, id) == nil { n := NewTreeNode(gvr, id) validate(f, n, optional) diff --git a/internal/xray/container_test.go b/internal/xray/container_test.go index 0c78100b..5cbde50f 100644 --- a/internal/xray/container_test.go +++ b/internal/xray/container_test.go @@ -18,6 +18,7 @@ import ( "github.com/derailed/k9s/internal/watch" "github.com/derailed/k9s/internal/xray" "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" @@ -32,11 +33,11 @@ func init() { func TestCOConfigMapRefs(t *testing.T) { var re xray.Container - root := xray.NewTreeNode("root", "root") + root := xray.NewTreeNode(client.NewGVR("root"), "root") ctx := context.WithValue(context.Background(), xray.KeyParent, root) ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory()) - assert.Nil(t, re.Render(ctx, "", render.ContainerRes{Container: makeCMContainer("c1", false)})) + require.NoError(t, re.Render(ctx, "", render.ContainerRes{Container: makeCMContainer("c1", false)})) assert.Equal(t, xray.MissingRefStatus, root.Children[0].Children[0].Extras[xray.StatusKey]) } @@ -88,11 +89,11 @@ func TestCORefs(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { var re xray.Container - root := xray.NewTreeNode("root", "root") + root := xray.NewTreeNode(client.NewGVR("root"), "root") ctx := context.WithValue(context.Background(), xray.KeyParent, root) ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory()) - assert.Nil(t, re.Render(ctx, "", u.co)) + require.NoError(t, re.Render(ctx, "", u.co)) assert.Equal(t, u.level1, root.CountChildren()) assert.Equal(t, u.level2, root.Children[0].CountChildren()) assert.Equal(t, u.e, root.Children[0].Children[0].Extras[xray.StatusKey]) @@ -108,7 +109,7 @@ func makeFactory() testFactory { } type testFactory struct { - rows map[string][]runtime.Object + rows map[*client.GVR][]runtime.Object } var _ dao.Factory = testFactory{} @@ -117,7 +118,7 @@ func (f testFactory) Client() client.Connection { return nil } -func (f testFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) { +func (f testFactory) Get(gvr *client.GVR, _ string, _ bool, _ labels.Selector) (runtime.Object, error) { oo, ok := f.rows[gvr] if ok && len(oo) > 0 { return oo[0], nil @@ -125,7 +126,7 @@ func (f testFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runt return nil, nil } -func (f testFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) { +func (f testFactory) List(gvr *client.GVR, _ string, _ bool, _ labels.Selector) ([]runtime.Object, error) { oo, ok := f.rows[gvr] if ok { return oo, nil @@ -133,11 +134,11 @@ func (f testFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]run return nil, nil } -func (f testFactory) ForResource(ns, gvr string) (informers.GenericInformer, error) { +func (f testFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) { return nil, nil } -func (f testFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { +func (f testFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) { return nil, nil } func (f testFactory) WaitForCacheSync() {} @@ -242,11 +243,11 @@ func makeDoubleCMKeysContainer(n string, optional bool) *v1.Container { func load(t *testing.T, 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 } diff --git a/internal/xray/dp.go b/internal/xray/dp.go index 1acc99a0..37fd3e49 100644 --- a/internal/xray/dp.go +++ b/internal/xray/dp.go @@ -22,7 +22,7 @@ import ( type Deployment struct{} // Render renders an xray node. -func (d *Deployment) Render(ctx context.Context, ns string, o interface{}) error { +func (d *Deployment) Render(ctx context.Context, ns string, o any) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected Unstructured, but got %T", o) @@ -38,7 +38,7 @@ func (d *Deployment) Render(ctx context.Context, ns string, o interface{}) error return fmt.Errorf("expecting a TreeNode but got %T", ctx.Value(KeyParent)) } - root := NewTreeNode("apps/v1/deployments", client.FQN(dp.Namespace, dp.Name)) + root := NewTreeNode(client.DpGVR, client.FQN(dp.Namespace, dp.Name)) oo, err := locatePods(ctx, dp.Namespace, dp.Spec.Selector) if err != nil { return err @@ -58,7 +58,7 @@ func (d *Deployment) Render(ctx context.Context, ns string, o interface{}) error if root.IsLeaf() { return nil } - gvr, nsID := "v1/namespaces", client.FQN(client.ClusterScope, dp.Namespace) + gvr, nsID := client.NsGVR, client.FQN(client.ClusterScope, dp.Namespace) nsn := parent.Find(gvr, nsID) if nsn == nil { nsn = NewTreeNode(gvr, nsID) @@ -102,5 +102,5 @@ func locatePods(ctx context.Context, ns string, sel *metav1.LabelSelector) ([]ru return nil, fmt.Errorf("expecting a factory but got %T", ctx.Value(internal.KeyFactory)) } - return f.List("v1/pods", ns, false, fsel.AsSelector()) + return f.List(client.PodGVR, ns, false, fsel.AsSelector()) } diff --git a/internal/xray/dp_test.go b/internal/xray/dp_test.go index 7286e9dd..a4024347 100644 --- a/internal/xray/dp_test.go +++ b/internal/xray/dp_test.go @@ -8,8 +8,10 @@ import ( "testing" "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime" ) @@ -30,19 +32,19 @@ func TestDeployRender(t *testing.T) { var re xray.Deployment for k := range uu { f := makeFactory() - f.rows = map[string][]runtime.Object{ - "v1/pods": {load(t, "po")}, - "v1/serviceaccounts": {load(t, "sa")}, + f.rows = map[*client.GVR][]runtime.Object{ + client.PodGVR: {load(t, "po")}, + client.SaGVR: {load(t, "sa")}, } u := uu[k] t.Run(k, func(t *testing.T) { o := load(t, u.file) - root := xray.NewTreeNode("deployments", "deployments") + root := xray.NewTreeNode(client.DpGVR, "deployments") ctx := context.WithValue(context.Background(), xray.KeyParent, root) ctx = context.WithValue(ctx, internal.KeyFactory, f) - assert.Nil(t, re.Render(ctx, "", o)) + require.NoError(t, re.Render(ctx, "", o)) assert.Equal(t, u.level1, root.CountChildren()) assert.Equal(t, u.level2, root.Children[0].CountChildren()) }) diff --git a/internal/xray/ds.go b/internal/xray/ds.go index 44eaad06..27d6d29f 100644 --- a/internal/xray/ds.go +++ b/internal/xray/ds.go @@ -18,7 +18,7 @@ import ( type DaemonSet struct{} // Render renders an xray node. -func (d *DaemonSet) Render(ctx context.Context, ns string, o interface{}) error { +func (d *DaemonSet) Render(ctx context.Context, ns string, o any) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected Unstructured, but got %T", o) @@ -34,7 +34,7 @@ func (d *DaemonSet) Render(ctx context.Context, ns string, o interface{}) error return fmt.Errorf("expecting a TreeNode but got %T", ctx.Value(KeyParent)) } - root := NewTreeNode("apps/v1/daemonsets", client.FQN(ds.Namespace, ds.Name)) + root := NewTreeNode(client.DsGVR, client.FQN(ds.Namespace, ds.Name)) oo, err := locatePods(ctx, ds.Namespace, ds.Spec.Selector) if err != nil { return err @@ -54,7 +54,7 @@ func (d *DaemonSet) Render(ctx context.Context, ns string, o interface{}) error if root.IsLeaf() { return nil } - gvr, nsID := "v1/namespaces", client.FQN(client.ClusterScope, ds.Namespace) + gvr, nsID := client.NsGVR, client.FQN(client.ClusterScope, ds.Namespace) nsn := parent.Find(gvr, nsID) if nsn == nil { nsn = NewTreeNode(gvr, nsID) diff --git a/internal/xray/ds_test.go b/internal/xray/ds_test.go index 40c4db11..27dae15a 100644 --- a/internal/xray/ds_test.go +++ b/internal/xray/ds_test.go @@ -8,8 +8,10 @@ import ( "testing" "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime" ) @@ -30,15 +32,15 @@ func TestDaemonSetRender(t *testing.T) { var re xray.DaemonSet for k := range uu { f := makeFactory() - f.rows = map[string][]runtime.Object{"v1/pods": {load(t, "po")}} + f.rows = map[*client.GVR][]runtime.Object{client.PodGVR: {load(t, "po")}} u := uu[k] t.Run(k, func(t *testing.T) { o := load(t, u.file) - root := xray.NewTreeNode("daemonsets", "daemonsets") + root := xray.NewTreeNode(client.DsGVR, "daemonsets") ctx := context.WithValue(context.Background(), xray.KeyParent, root) ctx = context.WithValue(ctx, internal.KeyFactory, f) - assert.Nil(t, re.Render(ctx, "", o)) + require.NoError(t, re.Render(ctx, "", o)) assert.Equal(t, u.level1, root.CountChildren()) assert.Equal(t, u.level2, root.Children[0].CountChildren()) }) diff --git a/internal/xray/generic.go b/internal/xray/generic.go index 8d30badc..7deb52a5 100644 --- a/internal/xray/generic.go +++ b/internal/xray/generic.go @@ -22,7 +22,7 @@ func (g *Generic) SetTable(_ string, t *metav1.Table) { } // Render renders a K8s resource to screen. -func (g *Generic) Render(ctx context.Context, ns string, o interface{}) error { +func (g *Generic) Render(ctx context.Context, ns string, o any) error { row, ok := o.(metav1.TableRow) if !ok { return fmt.Errorf("expecting a TableRow but got %T", o) @@ -33,7 +33,7 @@ func (g *Generic) Render(ctx context.Context, ns string, o interface{}) error { return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0]) } - root := NewTreeNode("generic", client.FQN(ns, n)) + root := NewTreeNode(client.NewGVR("generic"), client.FQN(ns, n)) parent, ok := ctx.Value(KeyParent).(*TreeNode) if !ok { return fmt.Errorf("expecting TreeNode but got %T", ctx.Value(KeyParent)) diff --git a/internal/xray/generic_test.go b/internal/xray/generic_test.go index 61b1dc89..70befbaa 100644 --- a/internal/xray/generic_test.go +++ b/internal/xray/generic_test.go @@ -8,8 +8,10 @@ import ( "testing" "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" ) @@ -28,11 +30,11 @@ func TestGenericRender(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - root := xray.NewTreeNode("generics", "generics") + root := xray.NewTreeNode(client.NewGVR("generic"), "generics") ctx := context.WithValue(context.Background(), xray.KeyParent, root) ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory()) - assert.Nil(t, re.Render(ctx, "", makeTable())) + require.NoError(t, re.Render(ctx, "", makeTable())) assert.Equal(t, u.level1, root.CountChildren()) }) } @@ -42,6 +44,6 @@ func TestGenericRender(t *testing.T) { func makeTable() metav1beta1.TableRow { return metav1beta1.TableRow{ - Cells: []interface{}{"fred", "blee"}, + Cells: []any{"fred", "blee"}, } } diff --git a/internal/xray/ns.go b/internal/xray/ns.go index bb7dd059..2d2c797d 100644 --- a/internal/xray/ns.go +++ b/internal/xray/ns.go @@ -17,7 +17,7 @@ import ( type Namespace struct{} // Render renders an xray node. -func (n *Namespace) Render(ctx context.Context, ns string, o interface{}) error { +func (n *Namespace) Render(ctx context.Context, ns string, o any) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected NamespaceWithMetrics, but got %T", o) @@ -29,7 +29,7 @@ func (n *Namespace) Render(ctx context.Context, ns string, o interface{}) error return err } - root := NewTreeNode("v1/namespaces", client.FQN(client.ClusterScope, nss.Name)) + root := NewTreeNode(client.NsGVR, client.FQN(client.ClusterScope, nss.Name)) parent, ok := ctx.Value(KeyParent).(*TreeNode) if !ok { return fmt.Errorf("expecting a TreeNode but got %T", ctx.Value(KeyParent)) diff --git a/internal/xray/ns_test.go b/internal/xray/ns_test.go index ab214e92..7a3ef1ee 100644 --- a/internal/xray/ns_test.go +++ b/internal/xray/ns_test.go @@ -8,8 +8,10 @@ import ( "testing" "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNamespaceRender(t *testing.T) { @@ -30,11 +32,11 @@ func TestNamespaceRender(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { o := load(t, u.file) - root := xray.NewTreeNode("namespaces", "namespaces") + root := xray.NewTreeNode(client.NsGVR, "namespaces") ctx := context.WithValue(context.Background(), xray.KeyParent, root) ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory()) - assert.Nil(t, re.Render(ctx, "", o)) + require.NoError(t, re.Render(ctx, "", o)) assert.Equal(t, u.level1, root.CountChildren()) }) } diff --git a/internal/xray/pod.go b/internal/xray/pod.go index 9fb27272..0771b5f4 100644 --- a/internal/xray/pod.go +++ b/internal/xray/pod.go @@ -21,7 +21,7 @@ import ( type Pod struct{} // Render renders an xray node. -func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error { +func (p *Pod) Render(ctx context.Context, ns string, o any) error { pwm, ok := o.(*render.PodWithMetrics) if !ok { return fmt.Errorf("expected PodWithMetrics, but got %T", o) @@ -38,21 +38,21 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error { return fmt.Errorf("no factory found in context") } - node := NewTreeNode("v1/pods", client.FQN(po.Namespace, po.Name)) + node := NewTreeNode(client.PodGVR, client.FQN(po.Namespace, po.Name)) parent, ok := ctx.Value(KeyParent).(*TreeNode) if !ok { return fmt.Errorf("expecting a TreeNode but got %T", ctx.Value(KeyParent)) } - if err := p.containerRefs(ctx, node, po.Namespace, po.Spec); err != nil { + if err := p.containerRefs(ctx, node, po.Namespace, &po.Spec); err != nil { return err } p.podVolumeRefs(f, node, po.Namespace, po.Spec.Volumes) - if err := p.serviceAccountRef(ctx, f, node, po.Namespace, po.Spec); err != nil { + if err := p.serviceAccountRef(ctx, f, node, po.Namespace, &po.Spec); err != nil { return err } - gvr, nsID := "v1/namespaces", client.FQN(client.ClusterScope, po.Namespace) + gvr, nsID := client.NsGVR, client.FQN(client.ClusterScope, po.Namespace) nsn := parent.Find(gvr, nsID) if nsn == nil { nsn = NewTreeNode(gvr, nsID) @@ -65,9 +65,9 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error { func (p *Pod) validate(node *TreeNode, po v1.Pod) error { var re render.Pod - phase := re.Phase(&po) + phase := re.Phase(po.DeletionTimestamp, &po.Spec, &po.Status) ss := po.Status.ContainerStatuses - cr, _, _ := re.Statuses(ss) + cr, _, _, _ := re.Statuses(ss) status := OkStatus if cr != len(ss) { status = ToastStatus @@ -82,20 +82,20 @@ func (p *Pod) validate(node *TreeNode, po v1.Pod) error { return nil } -func (*Pod) containerRefs(ctx context.Context, parent *TreeNode, ns string, spec v1.PodSpec) error { +func (*Pod) containerRefs(ctx context.Context, parent *TreeNode, ns string, spec *v1.PodSpec) error { ctx = context.WithValue(ctx, KeyParent, parent) var cre Container - for i := 0; i < len(spec.InitContainers); i++ { + for i := range len(spec.InitContainers) { if err := cre.Render(ctx, ns, render.ContainerRes{Container: &spec.InitContainers[i]}); err != nil { return err } } - for i := 0; i < len(spec.Containers); i++ { + for i := range len(spec.Containers) { if err := cre.Render(ctx, ns, render.ContainerRes{Container: &spec.Containers[i]}); err != nil { return err } } - for i := 0; i < len(spec.EphemeralContainers); i++ { + for i := range len(spec.EphemeralContainers) { if err := cre.Render(ctx, ns, render.ContainerRes{Container: &spec.Containers[i]}); err != nil { return err } @@ -104,18 +104,18 @@ func (*Pod) containerRefs(ctx context.Context, parent *TreeNode, ns string, spec return nil } -func (*Pod) serviceAccountRef(ctx context.Context, f dao.Factory, parent *TreeNode, ns string, spec v1.PodSpec) error { +func (*Pod) serviceAccountRef(ctx context.Context, f dao.Factory, parent *TreeNode, ns string, spec *v1.PodSpec) error { if spec.ServiceAccountName == "" { return nil } - id := client.FQN(ns, spec.ServiceAccountName) - o, err := f.Get("v1/serviceaccounts", id, true, labels.Everything()) + fqn := client.FQN(ns, spec.ServiceAccountName) + o, err := f.Get(client.SaGVR, fqn, true, labels.Everything()) if err != nil { return err } if o == nil { - addRef(f, parent, "v1/serviceaccounts", id, nil) + addRef(f, parent, client.SaGVR, fqn, nil) return nil } @@ -126,22 +126,22 @@ func (*Pod) serviceAccountRef(ctx context.Context, f dao.Factory, parent *TreeNo } func (*Pod) podVolumeRefs(f dao.Factory, parent *TreeNode, ns string, vv []v1.Volume) { - for _, v := range vv { - sec := v.Secret + for i := range vv { + sec := vv[i].Secret if sec != nil { - addRef(f, parent, "v1/secrets", client.FQN(ns, sec.SecretName), sec.Optional) + addRef(f, parent, client.SecGVR, client.FQN(ns, sec.SecretName), sec.Optional) continue } - cm := v.ConfigMap + cm := vv[i].ConfigMap if cm != nil { - addRef(f, parent, "v1/configmaps", client.FQN(ns, cm.Name), cm.Optional) + addRef(f, parent, client.CmGVR, client.FQN(ns, cm.Name), cm.Optional) continue } - pvc := v.PersistentVolumeClaim + pvc := vv[i].PersistentVolumeClaim if pvc != nil { - addRef(f, parent, "v1/persistentvolumeclaims", client.FQN(ns, pvc.ClaimName), nil) + addRef(f, parent, client.PvcGVR, client.FQN(ns, pvc.ClaimName), nil) } } } diff --git a/internal/xray/pod_test.go b/internal/xray/pod_test.go index 23dd18bc..92a08051 100644 --- a/internal/xray/pod_test.go +++ b/internal/xray/pod_test.go @@ -8,9 +8,11 @@ import ( "testing" "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPodRender(t *testing.T) { @@ -44,13 +46,13 @@ func TestPodRender(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { o := load(t, u.file) - root := xray.NewTreeNode("pods", "pods") + root := xray.NewTreeNode(client.PodGVR, "pods") ctx := context.WithValue(context.Background(), xray.KeyParent, root) ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory()) - assert.Nil(t, re.Render(ctx, "", &render.PodWithMetrics{Raw: o})) + require.NoError(t, re.Render(ctx, "", &render.PodWithMetrics{Raw: o})) assert.Equal(t, u.children, root.CountChildren()) - assert.Equal(t, u.count, root.Count("")) + assert.Equal(t, u.count, root.Count(client.NoGVR)) }) } } diff --git a/internal/xray/rs.go b/internal/xray/rs.go index 2078d74b..b547569c 100644 --- a/internal/xray/rs.go +++ b/internal/xray/rs.go @@ -18,7 +18,7 @@ import ( type ReplicaSet struct{} // Render renders an xray node. -func (r *ReplicaSet) Render(ctx context.Context, ns string, o interface{}) error { +func (r *ReplicaSet) Render(ctx context.Context, ns string, o any) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected Unstructured, but got %T", o) @@ -34,7 +34,7 @@ func (r *ReplicaSet) Render(ctx context.Context, ns string, o interface{}) error return fmt.Errorf("expecting a TreeNode but got %T", ctx.Value(KeyParent)) } - root := NewTreeNode("apps/v1/replicasets", client.FQN(rs.Namespace, rs.Name)) + root := NewTreeNode(client.RsGVR, client.FQN(rs.Namespace, rs.Name)) oo, err := locatePods(ctx, rs.Namespace, rs.Spec.Selector) if err != nil { return err @@ -56,7 +56,7 @@ func (r *ReplicaSet) Render(ctx context.Context, ns string, o interface{}) error return nil } - gvr, nsID := "v1/namespaces", client.FQN(client.ClusterScope, rs.Namespace) + gvr, nsID := client.NsGVR, client.FQN(client.ClusterScope, rs.Namespace) nsn := parent.Find(gvr, nsID) if nsn == nil { nsn = NewTreeNode(gvr, nsID) diff --git a/internal/xray/rs_test.go b/internal/xray/rs_test.go index 52e739f1..4868b291 100644 --- a/internal/xray/rs_test.go +++ b/internal/xray/rs_test.go @@ -8,8 +8,10 @@ import ( "testing" "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime" ) @@ -32,14 +34,16 @@ func TestReplicaSetRender(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { f := makeFactory() - f.rows = map[string][]runtime.Object{"v1/pods": {load(t, "po")}} + f.rows = map[*client.GVR][]runtime.Object{ + client.PodGVR: {load(t, "po")}, + } o := load(t, u.file) - root := xray.NewTreeNode("replicasets", "replicasets") + root := xray.NewTreeNode(client.RsGVR, "replicasets") ctx := context.WithValue(context.Background(), xray.KeyParent, root) ctx = context.WithValue(ctx, internal.KeyFactory, f) - assert.Nil(t, re.Render(ctx, "", o)) + require.NoError(t, re.Render(ctx, "", o)) assert.Equal(t, u.level1, root.CountChildren()) assert.Equal(t, u.level2, root.Children[0].CountChildren()) }) diff --git a/internal/xray/sa.go b/internal/xray/sa.go index 175a996a..647b4f02 100644 --- a/internal/xray/sa.go +++ b/internal/xray/sa.go @@ -19,7 +19,7 @@ import ( type ServiceAccount struct{} // Render renders an xray node. -func (s *ServiceAccount) Render(ctx context.Context, ns string, o interface{}) error { +func (s *ServiceAccount) Render(ctx context.Context, ns string, o any) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("ServiceAccount render expecting *Unstructured, but got %T", o) @@ -35,7 +35,7 @@ func (s *ServiceAccount) Render(ctx context.Context, ns string, o interface{}) e if !ok { return fmt.Errorf("no factory found in context") } - node := NewTreeNode("v1/serviceaccounts", client.FQN(sa.Namespace, sa.Name)) + node := NewTreeNode(client.SaGVR, client.FQN(sa.Namespace, sa.Name)) parent, ok := ctx.Value(KeyParent).(*TreeNode) if !ok { @@ -44,10 +44,10 @@ func (s *ServiceAccount) Render(ctx context.Context, ns string, o interface{}) e parent.Add(node) for _, sec := range sa.Secrets { - addRef(f, node, "v1/secrets", client.FQN(sa.Namespace, sec.Name), nil) + addRef(f, node, client.SecGVR, client.FQN(sa.Namespace, sec.Name), nil) } for _, sec := range sa.ImagePullSecrets { - addRef(f, node, "v1/secrets", client.FQN(sa.Namespace, sec.Name), nil) + addRef(f, node, client.SecGVR, client.FQN(sa.Namespace, sec.Name), nil) } auto, _ := ctx.Value(KeySAAutomount).(*bool) diff --git a/internal/xray/sa_test.go b/internal/xray/sa_test.go index 7afdf0de..65156f28 100644 --- a/internal/xray/sa_test.go +++ b/internal/xray/sa_test.go @@ -8,8 +8,10 @@ import ( "testing" "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSARender(t *testing.T) { @@ -31,11 +33,11 @@ func TestSARender(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { o := load(t, u.file) - root := xray.NewTreeNode("serviceaccounts", "serviceaccounts") + root := xray.NewTreeNode(client.SaGVR, "serviceaccounts") ctx := context.WithValue(context.Background(), xray.KeyParent, root) ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory()) - assert.Nil(t, re.Render(ctx, "", o)) + require.NoError(t, re.Render(ctx, "", o)) assert.Equal(t, u.level1, root.CountChildren()) assert.Equal(t, u.level2, root.Children[0].CountChildren()) }) diff --git a/internal/xray/section.go b/internal/xray/section.go index cc6d04ae..089c11ce 100644 --- a/internal/xray/section.go +++ b/internal/xray/section.go @@ -8,6 +8,7 @@ import ( "fmt" "strings" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/render" ) @@ -17,12 +18,12 @@ type Section struct { } // Render renders an xray node. -func (s *Section) Render(ctx context.Context, ns string, o interface{}) error { +func (s *Section) Render(ctx context.Context, ns string, o any) error { section, ok := o.(render.Section) if !ok { return fmt.Errorf("expected Section, but got %T", o) } - root := NewTreeNode(section.GVR, section.Title) + root := NewTreeNode(client.NewGVR(section.GVR), section.Title) parent, ok := ctx.Value(KeyParent).(*TreeNode) if !ok { return fmt.Errorf("expecting a TreeNode but got %T", ctx.Value(KeyParent)) @@ -35,20 +36,20 @@ func (s *Section) Render(ctx context.Context, ns string, o interface{}) error { func (*Section) outcomeRefs(parent *TreeNode, section render.Section) { for k, issues := range section.Outcome { - p := NewTreeNode(section.GVR, cleanse(k)) + p := NewTreeNode(client.NewGVR(section.GVR), cleanse(k)) parent.Add(p) for _, issue := range issues { msg := colorize(cleanse(issue.Message), issue.Level) - c := NewTreeNode(fmt.Sprintf("issue_%d", issue.Level), msg) + c := NewTreeNode(client.NewGVR(fmt.Sprintf("issue_%d", issue.Level)), msg) if issue.Group == "__root__" { p.Add(c) continue } - if pa := p.Find(issue.GVR, issue.Group); pa != nil { + if pa := p.Find(client.NewGVR(issue.GVR), issue.Group); pa != nil { pa.Add(c) continue } - pa := NewTreeNode(issue.GVR, issue.Group) + pa := NewTreeNode(client.NewGVR(issue.GVR), issue.Group) pa.Add(c) p.Add(pa) } diff --git a/internal/xray/sts.go b/internal/xray/sts.go index 78ab796c..3ff66959 100644 --- a/internal/xray/sts.go +++ b/internal/xray/sts.go @@ -18,7 +18,7 @@ import ( type StatefulSet struct{} // Render renders an xray node. -func (s *StatefulSet) Render(ctx context.Context, ns string, o interface{}) error { +func (s *StatefulSet) Render(ctx context.Context, ns string, o any) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected Unstructured, but got %T", o) @@ -34,7 +34,7 @@ func (s *StatefulSet) Render(ctx context.Context, ns string, o interface{}) erro return fmt.Errorf("expecting a TreeNode but got %T", ctx.Value(KeyParent)) } - root := NewTreeNode("apps/v1/statefulsets", client.FQN(sts.Namespace, sts.Name)) + root := NewTreeNode(client.StsGVR, client.FQN(sts.Namespace, sts.Name)) oo, err := locatePods(ctx, sts.Namespace, sts.Spec.Selector) if err != nil { return err @@ -56,7 +56,7 @@ func (s *StatefulSet) Render(ctx context.Context, ns string, o interface{}) erro return nil } - gvr, nsID := "v1/namespaces", client.FQN(client.ClusterScope, sts.Namespace) + gvr, nsID := client.NsGVR, client.FQN(client.ClusterScope, sts.Namespace) nsn := parent.Find(gvr, nsID) if nsn == nil { nsn = NewTreeNode(gvr, nsID) diff --git a/internal/xray/sts_test.go b/internal/xray/sts_test.go index 7d044471..868c4e1a 100644 --- a/internal/xray/sts_test.go +++ b/internal/xray/sts_test.go @@ -8,8 +8,10 @@ import ( "testing" "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime" ) @@ -32,14 +34,14 @@ func TestStatefulSetRender(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { f := makeFactory() - f.rows = map[string][]runtime.Object{"v1/pods": {load(t, "po")}} + f.rows = map[*client.GVR][]runtime.Object{client.PodGVR: {load(t, "po")}} o := load(t, u.file) - root := xray.NewTreeNode("statefulsets", "statefulsets") + root := xray.NewTreeNode(client.StsGVR, "statefulsets") ctx := context.WithValue(context.Background(), xray.KeyParent, root) ctx = context.WithValue(ctx, internal.KeyFactory, f) - assert.Nil(t, re.Render(ctx, "", o)) + require.NoError(t, re.Render(ctx, "", o)) assert.Equal(t, u.level1, root.CountChildren()) assert.Equal(t, u.level2, root.Children[0].CountChildren()) }) diff --git a/internal/xray/svc.go b/internal/xray/svc.go index 59a97682..90577b7f 100644 --- a/internal/xray/svc.go +++ b/internal/xray/svc.go @@ -22,7 +22,7 @@ import ( type Service struct{} // Render renders an xray node. -func (s *Service) Render(ctx context.Context, ns string, o interface{}) error { +func (s *Service) Render(ctx context.Context, ns string, o any) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected Unstructured, but got %T", o) @@ -39,7 +39,7 @@ func (s *Service) Render(ctx context.Context, ns string, o interface{}) error { return fmt.Errorf("expecting a TreeNode but got %T", ctx.Value(KeyParent)) } - root := NewTreeNode("v1/services", client.FQN(svc.Namespace, svc.Name)) + root := NewTreeNode(client.SvcGVR, client.FQN(svc.Namespace, svc.Name)) oo, err := s.locatePods(ctx, svc.Namespace, svc.Spec.Selector) if err != nil { return err @@ -60,7 +60,7 @@ func (s *Service) Render(ctx context.Context, ns string, o interface{}) error { if root.IsLeaf() { return nil } - gvr, nsID := "v1/namespaces", client.FQN(client.ClusterScope, svc.Namespace) + gvr, nsID := client.NsGVR, client.FQN(client.ClusterScope, svc.Namespace) nsn := parent.Find(gvr, nsID) if nsn == nil { nsn = NewTreeNode(gvr, nsID) @@ -87,5 +87,5 @@ func (s *Service) locatePods(ctx context.Context, ns string, sel map[string]stri return nil, err } - return f.List("v1/pods", ns, false, fsel.AsSelector()) + return f.List(client.PodGVR, ns, false, fsel.AsSelector()) } diff --git a/internal/xray/svc_test.go b/internal/xray/svc_test.go index 14be39e5..a296b6de 100644 --- a/internal/xray/svc_test.go +++ b/internal/xray/svc_test.go @@ -8,8 +8,10 @@ import ( "testing" "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime" ) @@ -30,16 +32,16 @@ func TestServiceRender(t *testing.T) { var re xray.Service for k := range uu { f := makeFactory() - f.rows = map[string][]runtime.Object{"v1/pods": {load(t, "po")}} + f.rows = map[*client.GVR][]runtime.Object{client.PodGVR: {load(t, "po")}} u := uu[k] t.Run(k, func(t *testing.T) { o := load(t, u.file) - root := xray.NewTreeNode("services", "services") + root := xray.NewTreeNode(client.SvcGVR, "services") ctx := context.WithValue(context.Background(), xray.KeyParent, root) ctx = context.WithValue(ctx, internal.KeyFactory, f) - assert.Nil(t, re.Render(ctx, "", o)) + require.NoError(t, re.Render(ctx, "", o)) assert.Equal(t, u.level1, root.CountChildren()) assert.Equal(t, u.level2, root.Children[0].CountChildren()) }) diff --git a/internal/xray/tree_node.go b/internal/xray/tree_node.go index 44b86970..eca8a928 100644 --- a/internal/xray/tree_node.go +++ b/internal/xray/tree_node.go @@ -54,14 +54,16 @@ type TreeRef string // NodeSpec represents a node resource specification. type NodeSpec struct { - GVRs, Paths, Statuses []string + GVRs client.GVRs + Paths, Statuses []string } // ParentGVR returns the parent GVR. -func (s NodeSpec) ParentGVR() *string { +func (s NodeSpec) ParentGVR() *client.GVR { if len(s.GVRs) > 1 { - return &s.GVRs[1] + return s.GVRs[1] } + return nil } @@ -74,7 +76,7 @@ func (s NodeSpec) ParentPath() *string { } // GVR returns the current GVR. -func (s NodeSpec) GVR() string { +func (s NodeSpec) GVR() *client.GVR { return s.GVRs[0] } @@ -95,7 +97,12 @@ func (s NodeSpec) AsPath() string { // AsGVR returns a gvr hierarchy as string. func (s NodeSpec) AsGVR() string { - return strings.Join(s.GVRs, PathSeparator) + ss := make([]string, 0, len(s.GVRs)) + for _, gvr := range s.GVRs { + ss = append(ss, gvr.R()) + } + + return strings.Join(ss, PathSeparator) } // AsStatus returns a status hierarchy as string. @@ -129,14 +136,15 @@ func (c ChildNodes) Less(i, j int) bool { // TreeNode represents a resource tree node. type TreeNode struct { - GVR, ID string + GVR *client.GVR + ID string Children ChildNodes Parent *TreeNode Extras map[string]string } // NewTreeNode returns a new instance. -func NewTreeNode(gvr, id string) *TreeNode { +func NewTreeNode(gvr *client.GVR, id string) *TreeNode { return &TreeNode{ GVR: gvr, ID: id, @@ -150,9 +158,9 @@ func (t *TreeNode) CountChildren() int { } // Count all the nodes from this node. -func (t *TreeNode) Count(gvr string) int { +func (t *TreeNode) Count(gvr *client.GVR) int { counter := 0 - if t.GVR == gvr || gvr == "" { + if t.GVR == gvr || gvr == client.NoGVR { counter++ } for _, c := range t.Children { @@ -192,17 +200,18 @@ func (t *TreeNode) Sort() { // Spec returns this node specification. func (t *TreeNode) Spec() NodeSpec { - var GVRs, Paths, Statuses []string + var gvrs client.GVRs + var paths, statuses []string for parent := t; parent != nil; parent = parent.Parent { - GVRs = append(GVRs, parent.GVR) - Paths = append(Paths, parent.ID) - Statuses = append(Statuses, parent.Extras[StatusKey]) + gvrs = append(gvrs, parent.GVR) + paths = append(paths, parent.ID) + statuses = append(statuses, parent.Extras[StatusKey]) } return NodeSpec{ - GVRs: GVRs, - Paths: Paths, - Statuses: Statuses, + GVRs: gvrs, + Paths: paths, + Statuses: statuses, } } @@ -221,12 +230,12 @@ func (t *TreeNode) Flatten() []NodeSpec { // Blank returns true if this node is unset. func (t *TreeNode) Blank() bool { - return t.GVR == "" && t.ID == "" + return t.GVR == client.NoGVR && t.ID == "" } // Hydrate hydrates a full tree bases on a collection of specifications. func Hydrate(specs []NodeSpec) *TreeNode { - root := NewTreeNode("", "") + root := NewTreeNode(client.NoGVR, "") nav := root for _, spec := range specs { for i := len(spec.Paths) - 1; i >= 0; i-- { @@ -325,7 +334,7 @@ func (t *TreeNode) Clear() { } // Find locates a node given a gvr/id spec. -func (t *TreeNode) Find(gvr, id string) *TreeNode { +func (t *TreeNode) Find(gvr *client.GVR, id string) *TreeNode { if t.GVR == gvr && t.ID == id { return t } @@ -377,8 +386,8 @@ func dumpStdOut(n *TreeNode, level int) { } } -func category(gvr string) string { - meta, err := dao.MetaAccess.MetaFor(client.NewGVR(gvr)) +func category(gvr *client.GVR) string { + meta, err := dao.MetaAccess.MetaFor(gvr) if err != nil { return "" } @@ -470,32 +479,32 @@ func (t TreeNode) toEmojiTitle() (title string) { return } -func toEmoji(gvr string) string { +func toEmoji(gvr *client.GVR) string { if e := v1Emoji(gvr); e != "" { return e } if e := appsEmoji(gvr); e != "" { return e } - if e := issueEmoji(gvr); e != "" { + if e := issueEmoji(gvr.String()); e != "" { return e } switch gvr { - case "autoscaling/v1/horizontalpodautoscalers": + case client.HpaGVR: return "โ™Ž๏ธ" - case "rbac.authorization.k8s.io/v1/clusterrolebindings", "rbac.authorization.k8s.io/v1/clusterroles": + case client.CrGVR, client.CrbGVR: return "๐Ÿ‘ฉโ€" - case "rbac.authorization.k8s.io/v1/rolebindings", "rbac.authorization.k8s.io/v1/roles": + case client.RoGVR, client.RobGVR: return "๐Ÿ‘จ๐Ÿปโ€" - case "networking.k8s.io/v1/networkpolicies": + case client.NpGVR: return "๐Ÿ“•" - case "policy/v1/poddisruptionbudgets": + case client.PdbGVR: return "๐Ÿท " - case "policy/v1beta1/podsecuritypolicies": + case client.PspGVR: return "๐Ÿ‘ฎโ€โ™‚๏ธ" - case "containers": + case client.CoGVR: return "๐Ÿณ" - case "report": + case client.NewGVR("report"): return "๐Ÿงผ" default: return "๐Ÿ“Ž" @@ -517,40 +526,40 @@ func issueEmoji(gvr string) string { } } -func v1Emoji(gvr string) string { +func v1Emoji(gvr *client.GVR) string { switch gvr { - case "v1/namespaces": + case client.NsGVR: return "๐Ÿ—‚ " - case "v1/nodes": + case client.NodeGVR: return "๐Ÿ–ฅ " - case "v1/pods": + case client.PodGVR: return "๐Ÿš›" - case "v1/services": + case client.SvcGVR: return "๐Ÿ’โ€โ™€๏ธ" - case "v1/serviceaccounts": + case client.SaGVR: return "๐Ÿ’ณ" - case "v1/persistentvolumes": + case client.PvGVR: return "๐Ÿ“š" - case "v1/persistentvolumeclaims": + case client.PvcGVR: return "๐ŸŽŸ " - case "v1/secrets": + case client.SecGVR: return "๐Ÿ”’" - case "v1/configmaps": + case client.CmGVR: return "๐Ÿ—บ " default: return "" } } -func appsEmoji(gvr string) string { +func appsEmoji(gvr *client.GVR) string { switch gvr { - case "apps/v1/deployments": + case client.DpGVR: return "๐Ÿช‚" - case "apps/v1/statefulsets": + case client.StsGVR: return "๐ŸŽŽ" - case "apps/v1/daemonsets": + case client.DsGVR: return "๐Ÿ˜ˆ" - case "apps/v1/replicasets": + case client.RsGVR: return "๐Ÿ‘ฏโ€โ™‚๏ธ" default: return "" @@ -559,24 +568,24 @@ func appsEmoji(gvr string) string { // EmojiInfo returns emoji help. func EmojiInfo() map[string]string { - GVRs := []string{ - "containers", - "v1/namespaces", - "v1/pods", - "v1/services", - "v1/serviceaccounts", - "v1/persistentvolumes", - "v1/persistentvolumeclaims", - "v1/secrets", - "v1/configmaps", - "apps/v1/deployments", - "apps/v1/statefulsets", - "apps/v1/daemonsets", + gvrs := []*client.GVR{ + client.CoGVR, + client.NsGVR, + client.PodGVR, + client.SvcGVR, + client.SaGVR, + client.PvGVR, + client.PvcGVR, + client.SecGVR, + client.CmGVR, + client.DpGVR, + client.StsGVR, + client.DsGVR, } - m := make(map[string]string, len(GVRs)) - for _, g := range GVRs { - m[client.NewGVR(g).R()] = toEmoji(g) + m := make(map[string]string, len(gvrs)) + for _, gvr := range gvrs { + m[gvr.R()] = toEmoji(gvr) } return m diff --git a/internal/xray/tree_node_test.go b/internal/xray/tree_node_test.go index cd3442c7..67722542 100644 --- a/internal/xray/tree_node_test.go +++ b/internal/xray/tree_node_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" ) @@ -30,7 +31,7 @@ func TestTreeNodeCount(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.root.Count("")) + assert.Equal(t, u.e, u.root.Count(client.NoGVR)) }) } } @@ -99,12 +100,12 @@ func TestTreeNodeHydrate(t *testing.T) { "flat_simple": { spec: []xray.NodeSpec{ { - GVRs: []string{"containers", "v1/pods"}, + GVRs: []*client.GVR{client.CoGVR, client.PodGVR}, Paths: []string{"c1", "default/p1"}, Statuses: threeOK, }, { - GVRs: []string{"containers", "v1/pods"}, + GVRs: []*client.GVR{client.CoGVR, client.PodGVR}, Paths: []string{"c2", "default/p1"}, Statuses: threeOK, }, @@ -114,12 +115,12 @@ func TestTreeNodeHydrate(t *testing.T) { "flat_complex": { spec: []xray.NodeSpec{ { - GVRs: []string{"v1/secrets", "containers", "v1/pods"}, + GVRs: []*client.GVR{client.SecGVR, client.CoGVR, client.PodGVR}, Paths: []string{"s1", "c1", "default/p1"}, Statuses: threeOK, }, { - GVRs: []string{"v1/secrets", "containers", "v1/pods"}, + GVRs: []*client.GVR{client.SecGVR, client.CoGVR, client.PodGVR}, Paths: []string{"s2", "c2", "default/p1"}, Statuses: threeOK, }, @@ -129,47 +130,47 @@ func TestTreeNodeHydrate(t *testing.T) { "complex1": { spec: []xray.NodeSpec{ { - GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + GVRs: []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR}, Paths: []string{"default/default-token-rr22g", "default/nginx-6b866d578b-c6tcn", "default/nginx", "-/default", "deployments"}, Statuses: fiveOK, }, { - GVRs: []string{"v1/configmaps", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + GVRs: []*client.GVR{client.CmGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR}, Paths: []string{"kube-system/coredns", "kube-system/coredns-6955765f44-89q2p", "kube-system/coredns", "-/kube-system", "deployments"}, Statuses: fiveOK, }, { - GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + GVRs: []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR}, Paths: []string{"kube-system/coredns-token-5cq9j", "kube-system/coredns-6955765f44-89q2p", "kube-system/coredns", "-/kube-system", "deployments"}, Statuses: fiveOK, }, { - GVRs: []string{"v1/configmaps", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + GVRs: []*client.GVR{client.CmGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR}, Paths: []string{"kube-system/coredns", "kube-system/coredns-6955765f44-r9j9t", "kube-system/coredns", "-/kube-system", "deployments"}, Statuses: fiveOK, }, { - GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + GVRs: []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR}, Paths: []string{"kube-system/coredns-token-5cq9j", "kube-system/coredns-6955765f44-r9j9t", "kube-system/coredns", "-/kube-system", "deployments"}, Statuses: fiveOK, }, { - GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + GVRs: []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR}, Paths: []string{"kube-system/default-token-thzt8", "kube-system/metrics-server-6754dbc9df-88bk4", "kube-system/metrics-server", "-/kube-system", "deployments"}, Statuses: fiveOK, }, { - GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + GVRs: []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR}, Paths: []string{"kube-system/nginx-ingress-token-kff5q", "kube-system/nginx-ingress-controller-6fc5bcc8c9-cwp55", "kube-system/nginx-ingress-controller", "-/kube-system", "deployments"}, Statuses: fiveOK, }, { - GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + GVRs: []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR}, Paths: []string{"kubernetes-dashboard/kubernetes-dashboard-token-d6rt4", "kubernetes-dashboard/dashboard-metrics-scraper-7b64584c5c-c7b56", "kubernetes-dashboard/dashboard-metrics-scraper", "-/kubernetes-dashboard", "deployments"}, Statuses: fiveOK, }, { - GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + GVRs: []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR}, Paths: []string{"kubernetes-dashboard/kubernetes-dashboard-token-d6rt4", "kubernetes-dashboard/kubernetes-dashboard-79d9cd965-b4c7d", "kubernetes-dashboard/kubernetes-dashboard", "-/kubernetes-dashboard", "deployments"}, Statuses: fiveOK, }, @@ -196,12 +197,12 @@ func TestTreeNodeFlatten(t *testing.T) { root: root1(), e: []xray.NodeSpec{ { - GVRs: []string{"containers", "v1/pods"}, + GVRs: []*client.GVR{client.CoGVR, client.PodGVR}, Paths: []string{"c1", "default/p1"}, Statuses: []string{"ok", "ok"}, }, { - GVRs: []string{"containers", "v1/pods"}, + GVRs: []*client.GVR{client.CoGVR, client.PodGVR}, Paths: []string{"c2", "default/p1"}, Statuses: []string{"ok", "ok"}, }, @@ -211,12 +212,12 @@ func TestTreeNodeFlatten(t *testing.T) { root: root2(), e: []xray.NodeSpec{ { - GVRs: []string{"v1/secrets", "containers", "v1/pods"}, + GVRs: []*client.GVR{client.SecGVR, client.CoGVR, client.PodGVR}, Paths: []string{"s1", "c1", "default/p1"}, Statuses: []string{"ok", "ok", "ok"}, }, { - GVRs: []string{"v1/secrets", "containers", "v1/pods"}, + GVRs: []*client.GVR{client.SecGVR, client.CoGVR, client.PodGVR}, Paths: []string{"s2", "c2", "default/p1"}, Statuses: []string{"ok", "ok", "ok"}, }, @@ -243,8 +244,8 @@ func TestTreeNodeDiff(t *testing.T) { n2: &xray.TreeNode{}, }, "same": { - n1: xray.NewTreeNode("v1/pods", "default/p1"), - n2: xray.NewTreeNode("v1/pods", "default/p1"), + n1: xray.NewTreeNode(client.PodGVR, "default/p1"), + n2: xray.NewTreeNode(client.PodGVR, "default/p1"), }, } @@ -257,8 +258,8 @@ func TestTreeNodeDiff(t *testing.T) { } func TestTreeNodeClone(t *testing.T) { - n := xray.NewTreeNode("v1/pods", "default/p1") - c1 := xray.NewTreeNode("containers", "c1") + n := xray.NewTreeNode(client.PodGVR, "default/p1") + c1 := xray.NewTreeNode(client.CoGVR, "c1") n.Add(c1) c := n.ShallowClone() @@ -266,9 +267,9 @@ func TestTreeNodeClone(t *testing.T) { } func TestTreeNodeRoot(t *testing.T) { - n := xray.NewTreeNode("v1/pods", "default/p1") - c1 := xray.NewTreeNode("containers", "c1") - c2 := xray.NewTreeNode("containers", "c2") + n := xray.NewTreeNode(client.PodGVR, "default/p1") + c1 := xray.NewTreeNode(client.CoGVR, "c1") + c2 := xray.NewTreeNode(client.CoGVR, "c2") n.Add(c1) n.Add(c2) @@ -283,9 +284,9 @@ func TestTreeNodeRoot(t *testing.T) { } func TestTreeNodeLevel(t *testing.T) { - n := xray.NewTreeNode("v1/pods", "default/p1") - c1 := xray.NewTreeNode("containers", "c1") - c2 := xray.NewTreeNode("containers", "c2") + n := xray.NewTreeNode(client.PodGVR, "default/p1") + c1 := xray.NewTreeNode(client.CoGVR, "c1") + c2 := xray.NewTreeNode(client.CoGVR, "c2") n.Add(c1) n.Add(c2) @@ -295,9 +296,9 @@ func TestTreeNodeLevel(t *testing.T) { } func TestTreeNodeMaxDepth(t *testing.T) { - n := xray.NewTreeNode("v1/pods", "default/p1") - c1 := xray.NewTreeNode("containers", "c1") - c2 := xray.NewTreeNode("containers", "c2") + n := xray.NewTreeNode(client.PodGVR, "default/p1") + c1 := xray.NewTreeNode(client.CoGVR, "c1") + c2 := xray.NewTreeNode(client.CoGVR, "c2") n.Add(c1) n.Add(c2) @@ -308,9 +309,9 @@ func TestTreeNodeMaxDepth(t *testing.T) { // Helpers... func root1() *xray.TreeNode { - n := xray.NewTreeNode("v1/pods", "default/p1") - c1 := xray.NewTreeNode("containers", "c1") - c2 := xray.NewTreeNode("containers", "c2") + n := xray.NewTreeNode(client.PodGVR, "default/p1") + c1 := xray.NewTreeNode(client.CoGVR, "c1") + c2 := xray.NewTreeNode(client.CoGVR, "c2") n.Add(c1) n.Add(c2) @@ -318,23 +319,23 @@ func root1() *xray.TreeNode { } func diff1() *xray.TreeNode { - n := xray.NewTreeNode("v1/pods", "default/p1") - c1 := xray.NewTreeNode("containers", "c1") + n := xray.NewTreeNode(client.PodGVR, "default/p1") + c1 := xray.NewTreeNode(client.CoGVR, "c1") n.Add(c1) return n } func root2() *xray.TreeNode { - c1 := xray.NewTreeNode("containers", "c1") - s1 := xray.NewTreeNode("v1/secrets", "s1") + c1 := xray.NewTreeNode(client.CoGVR, "c1") + s1 := xray.NewTreeNode(client.SecGVR, "s1") c1.Add(s1) - c2 := xray.NewTreeNode("containers", "c2") - s2 := xray.NewTreeNode("v1/secrets", "s2") + c2 := xray.NewTreeNode(client.CoGVR, "c2") + s2 := xray.NewTreeNode(client.SecGVR, "s2") c2.Add(s2) - n := xray.NewTreeNode("v1/pods", "default/p1") + n := xray.NewTreeNode(client.PodGVR, "default/p1") n.Add(c1) n.Add(c2) @@ -342,99 +343,99 @@ func root2() *xray.TreeNode { } func diff2() *xray.TreeNode { - n := xray.NewTreeNode("v1/pods", "default/p1") - c1 := xray.NewTreeNode("containers", "c2") + n := xray.NewTreeNode(client.PodGVR, "default/p1") + c1 := xray.NewTreeNode(client.CoGVR, "c2") n.Add(c1) - s1 := xray.NewTreeNode("v1/secrets", "s2") + s1 := xray.NewTreeNode(client.SecGVR, "s2") c1.Add(s1) return n } func root3() *xray.TreeNode { - n := xray.NewTreeNode("apps/v1/deployments", "deployments") + n := xray.NewTreeNode(client.DpGVR, "deployments") - ns1 := xray.NewTreeNode("v1/namespaces", "-/default") + ns1 := xray.NewTreeNode(client.NsGVR, "-/default") n.Add(ns1) { - d1 := xray.NewTreeNode("apps/v1/deployments", "default/nginx") + d1 := xray.NewTreeNode(client.DpGVR, "default/nginx") ns1.Add(d1) { - p1 := xray.NewTreeNode("v1/pods", "default/nginx-6b866d578b-c6tcn") + p1 := xray.NewTreeNode(client.PodGVR, "default/nginx-6b866d578b-c6tcn") d1.Add(p1) { - s1 := xray.NewTreeNode("v1/secrets", "default/default-token-rr22g") + s1 := xray.NewTreeNode(client.SecGVR, "default/default-token-rr22g") p1.Add(s1) } } } - ns2 := xray.NewTreeNode("v1/namespaces", "-/kube-system") + ns2 := xray.NewTreeNode(client.NsGVR, "-/kube-system") n.Add(ns2) { - d2 := xray.NewTreeNode("apps/v1/deployments", "kube-system/coredns") + d2 := xray.NewTreeNode(client.DpGVR, "kube-system/coredns") ns2.Add(d2) { - p2 := xray.NewTreeNode("v1/pods", "kube-system/coredns-6955765f44-89q2p") + p2 := xray.NewTreeNode(client.PodGVR, "kube-system/coredns-6955765f44-89q2p") d2.Add(p2) { - c1 := xray.NewTreeNode("v1/configmaps", "kube-system/coredns") + c1 := xray.NewTreeNode(client.CmGVR, "kube-system/coredns") p2.Add(c1) - s2 := xray.NewTreeNode("v1/secrets", "kube-system/coredns-token-5cq9j") + s2 := xray.NewTreeNode(client.SecGVR, "kube-system/coredns-token-5cq9j") p2.Add(s2) } - p3 := xray.NewTreeNode("v1/pods", "kube-system/coredns-6955765f44-r9j9t") + p3 := xray.NewTreeNode(client.PodGVR, "kube-system/coredns-6955765f44-r9j9t") d2.Add(p3) { - c2 := xray.NewTreeNode("v1/configmaps", "kube-system/coredns") + c2 := xray.NewTreeNode(client.CmGVR, "kube-system/coredns") p3.Add(c2) - s3 := xray.NewTreeNode("v1/secrets", "kube-system/coredns-token-5cq9j") + s3 := xray.NewTreeNode(client.SecGVR, "kube-system/coredns-token-5cq9j") p3.Add(s3) } } - d3 := xray.NewTreeNode("apps/v1/deployments", "kube-system/metrics-server") + d3 := xray.NewTreeNode(client.DpGVR, "kube-system/metrics-server") ns2.Add(d3) { - p3 := xray.NewTreeNode("v1/pods", "kube-system/metrics-server-6754dbc9df-88bk4") + p3 := xray.NewTreeNode(client.PodGVR, "kube-system/metrics-server-6754dbc9df-88bk4") d3.Add(p3) { - s4 := xray.NewTreeNode("v1/secrets", "kube-system/default-token-thzt8") + s4 := xray.NewTreeNode(client.SecGVR, "kube-system/default-token-thzt8") p3.Add(s4) } } - d4 := xray.NewTreeNode("apps/v1/deployments", "kube-system/nginx-ingress-controller") + d4 := xray.NewTreeNode(client.DpGVR, "kube-system/nginx-ingress-controller") ns2.Add(d4) { - p4 := xray.NewTreeNode("v1/pods", "kube-system/nginx-ingress-controller-6fc5bcc8c9-cwp55") + p4 := xray.NewTreeNode(client.PodGVR, "kube-system/nginx-ingress-controller-6fc5bcc8c9-cwp55") d4.Add(p4) { - s5 := xray.NewTreeNode("v1/secrets", "kube-system/nginx-ingress-token-kff5q") + s5 := xray.NewTreeNode(client.SecGVR, "kube-system/nginx-ingress-token-kff5q") p4.Add(s5) } } } - ns3 := xray.NewTreeNode("v1/namespaces", "-/kubernetes-dashboard") + ns3 := xray.NewTreeNode(client.NsGVR, "-/kubernetes-dashboard") n.Add(ns3) { - d5 := xray.NewTreeNode("apps/v1/deployments", "kubernetes-dashboard/dashboard-metrics-scraper") + d5 := xray.NewTreeNode(client.DpGVR, "kubernetes-dashboard/dashboard-metrics-scraper") ns3.Add(d5) { - p5 := xray.NewTreeNode("v1/pods", "kubernetes-dashboard/dashboard-metrics-scraper-7b64584c5c-c7b56") + p5 := xray.NewTreeNode(client.PodGVR, "kubernetes-dashboard/dashboard-metrics-scraper-7b64584c5c-c7b56") d5.Add(p5) { - s6 := xray.NewTreeNode("v1/secrets", "kubernetes-dashboard/kubernetes-dashboard-token-d6rt4") + s6 := xray.NewTreeNode(client.SecGVR, "kubernetes-dashboard/kubernetes-dashboard-token-d6rt4") p5.Add(s6) } } - d6 := xray.NewTreeNode("apps/v1/deployments", "kubernetes-dashboard/kubernetes-dashboard") + d6 := xray.NewTreeNode(client.DpGVR, "kubernetes-dashboard/kubernetes-dashboard") ns3.Add(d6) { - p6 := xray.NewTreeNode("v1/pods", "kubernetes-dashboard/kubernetes-dashboard-79d9cd965-b4c7d") + p6 := xray.NewTreeNode(client.PodGVR, "kubernetes-dashboard/kubernetes-dashboard-79d9cd965-b4c7d") d6.Add(p6) { - s6 := xray.NewTreeNode("v1/secrets", "kubernetes-dashboard/kubernetes-dashboard-token-d6rt4") + s6 := xray.NewTreeNode(client.SecGVR, "kubernetes-dashboard/kubernetes-dashboard-token-d6rt4") p6.Add(s6) } } @@ -444,27 +445,27 @@ func root3() *xray.TreeNode { } func diff3() *xray.TreeNode { - n := xray.NewTreeNode("apps/v1/deployments", "deployments") - ns2 := xray.NewTreeNode("v1/namespaces", "-/kube-system") + n := xray.NewTreeNode(client.DpGVR, "deployments") + ns2 := xray.NewTreeNode(client.NsGVR, "-/kube-system") n.Add(ns2) { - d2 := xray.NewTreeNode("apps/v1/deployments", "kube-system/coredns") + d2 := xray.NewTreeNode(client.DpGVR, "kube-system/coredns") ns2.Add(d2) { - p2 := xray.NewTreeNode("v1/pods", "kube-system/coredns-6955765f44-89q2p") + p2 := xray.NewTreeNode(client.PodGVR, "kube-system/coredns-6955765f44-89q2p") d2.Add(p2) { - c1 := xray.NewTreeNode("v1/configmaps", "kube-system/coredns") + c1 := xray.NewTreeNode(client.CmGVR, "kube-system/coredns") p2.Add(c1) - s2 := xray.NewTreeNode("v1/secrets", "kube-system/coredns-token-5cq9j") + s2 := xray.NewTreeNode(client.SecGVR, "kube-system/coredns-token-5cq9j") p2.Add(s2) } - p3 := xray.NewTreeNode("v1/pods", "kube-system/coredns-6955765f44-r9j9t") + p3 := xray.NewTreeNode(client.PodGVR, "kube-system/coredns-6955765f44-r9j9t") d2.Add(p3) { - c2 := xray.NewTreeNode("v1/configmaps", "kube-system/coredns") + c2 := xray.NewTreeNode(client.CmGVR, "kube-system/coredns") p3.Add(c2) - s3 := xray.NewTreeNode("v1/secrets", "kube-system/coredns-token-5cq9j") + s3 := xray.NewTreeNode(client.SecGVR, "kube-system/coredns-token-5cq9j") p3.Add(s3) } } diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 244c986c..00174b88 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: k9s base: core22 -version: 'v0.40.10' +version: 'v0.50.0' summary: K9s is a CLI to view and manage your Kubernetes clusters. description: | K9s is a CLI to view and manage your Kubernetes clusters. By leveraging a terminal UI, you can easily traverse Kubernetes resources and view the state of your clusters in a single powerful session.