diff --git a/.golangci.yml b/.golangci.yml index 561f3c6e..72fdf6e6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -239,77 +239,3 @@ formatters: - 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 - -# linters: -# default: none -# enable: -# - sloglint - -# exclusions: -# generated: lax -# paths: -# - third_party$ -# - builtin$ -# - examples$ -# - \\.(generated\\.deepcopy|pb)\\.go$ - -# settings: -# gocyclo: -# min-complexity: 35 - -# govet: -# enable: -# - nilness - -# goimports: -# local-prefixes: github.com/derailed/k9s - -# 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 - -# 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 - -# # 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: "SA9003: empty branch" - -# # - 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 7ed1066f..dfa4016f 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.50.3 +VERSION ?= v0.50.4 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} diff --git a/README.md b/README.md index 0e25415e..b3dbeb8d 100644 --- a/README.md +++ b/README.md @@ -409,8 +409,10 @@ You can now override the context portForward default address configuration by se liveViewAutoRefresh: false # The path to screen dump. Default: '%temp_dir%/k9s-screens-%username%' (k9s info) screenDumpDir: /tmp/dumps - # Represents ui poll intervals. Default 2secs + # Represents ui poll intervals in seconds. Default 2secs refreshRate: 2 + # Overrides the default k8s api server requests timeout. Defaults 120s + apiServerTimeout: 15s # Number of retries once the connection to the api-server is lost. Default 15. maxConnRetry: 5 # Indicates whether modification commands like delete/kill/edit are disabled. Default is false @@ -431,6 +433,7 @@ You can now override the context portForward default address configuration by se crumbsless: false # Set to true to suppress the K9s splash screen on start. Default false. Note that for larger clusters or higher latency connections, there may be no resources visible initially until local caches have finished populating. splashless: false + # Toggles icons display as not all terminal support these chars. Default: true noIcons: false # Toggles reactive UI. This option provide for watching on disk artifacts changes and update the UI live Defaults to false. reactive: false diff --git a/change_logs/release_v0.50.4.md b/change_logs/release_v0.50.4.md new file mode 100644 index 00000000..b5c24a2f --- /dev/null +++ b/change_logs/release_v0.50.4.md @@ -0,0 +1,44 @@ + + +# Release v0.50.4 + +## 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/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA) + +## Maintenance Release! + +--- + +## Resolved Issues + +* [#3288](https://github.com/derailed/k9s/issues/3288) Resource search doesn't filter by name in custom view +* [#3286](https://github.com/derailed/k9s/issues/3286) K9S doesn't understand matchExpressions selector in Deployment to Pod navigation +* [#3285](https://github.com/derailed/k9s/issues/3285) Rollout Restart method conflicts with GitOps (Flux, ArgoCD) +* [#3283](https://github.com/derailed/k9s/issues/3283) Deployment status showing wrong ready state +* [#3278](https://github.com/derailed/k9s/issues/3278) k9s doesn't honor the --namespace parameter + +--- + +## 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!! + +* [#3292](https://github.com/derailed/k9s/pull/3292) fix: respect insecure flag when switch context +* [#3277](https://github.com/derailed/k9s/pull/3277) feat: add hostPathVolume (docker) +* [#3253](https://github.com/derailed/k9s/pull/3253) fix: set default request timeout to 120 seconds +* [#2866](https://github.com/derailed/k9s/pull/2866) Feature/default_view + +--- + © 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/root.go b/cmd/root.go index 9e74a35b..793d7cc1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,18 +12,17 @@ import ( "strings" "time" - "github.com/lmittmann/tint" - "github.com/mattn/go-colorable" - "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/client-go/tools/clientcmd/api" - "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/color" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/slogs" "github.com/derailed/k9s/internal/view" + "github.com/lmittmann/tint" + "github.com/mattn/go-colorable" + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/tools/clientcmd/api" ) const ( diff --git a/go.mod b/go.mod index 76286e4d..8b187236 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.1 require ( github.com/adrg/xdg v0.5.3 github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716 - github.com/anchore/grype v0.87.0 + github.com/anchore/grype v0.91.0 github.com/anchore/syft v1.22.0 github.com/atotto/clipboard v0.1.4 github.com/cenkalti/backoff/v4 v4.3.0 @@ -152,7 +152,7 @@ require ( github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gdamore/encoding v1.0.1 // indirect github.com/github/go-spdx/v2 v2.3.2 // indirect - github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/glebarez/sqlite v1.11.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect @@ -232,8 +232,6 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect @@ -262,6 +260,7 @@ require ( github.com/openvex/go-vex v0.2.5 // indirect github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 // indirect github.com/package-url/packageurl-go v0.1.1 // indirect + github.com/pandatix/go-cvss v0.6.2 // indirect github.com/pborman/indent v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect @@ -336,7 +335,7 @@ require ( golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.30.0 // indirect - golang.org/x/time v0.8.0 // indirect + golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.31.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/api v0.215.0 // indirect diff --git a/go.sum b/go.sum index 2e0fa455..fb2b920d 100644 --- a/go.sum +++ b/go.sum @@ -712,8 +712,8 @@ github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0v github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ= github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 h1:rmZG77uXgE+o2gozGEBoUMpX27lsku+xrMwlmBZJtbg= github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= -github.com/anchore/grype v0.87.0 h1:cdrNA4rnSMpBxP8NmKNF75VUlP/VWaOG8YJJjiIDwIs= -github.com/anchore/grype v0.87.0/go.mod h1:Umw/9sZHnS+9mPLTnUFd/OzApR6dzE2CPlRsov7kSmQ= +github.com/anchore/grype v0.91.0 h1:x6/jweLDNp+jy6ufyCukBJbFAlVefxSUOqb2eFsJdQY= +github.com/anchore/grype v0.91.0/go.mod h1:O+lJcLV4OWTPwA8Rvr8U1utnqqNA0WgvVgc0q0QbpJw= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI= github.com/anchore/stereoscope v0.1.2 h1:0+Jcf7hoImYKfrH2XzN6vvbmbpm68A/woabC43gZ4eU= @@ -1001,8 +1001,8 @@ github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZ github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= github.com/gkampitakis/go-snaps v0.5.11 h1:LFG0ggUKR+KEiiaOvFCmLgJ5NO2zf93AxxddkBn3LdQ= github.com/gkampitakis/go-snaps v0.5.11/go.mod h1:PcKmy8q5Se7p48ywpogN5Td13reipz1Iivah4wrTIvY= -github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= -github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= @@ -1438,13 +1438,9 @@ github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJ github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= -github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -1523,6 +1519,8 @@ github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 h1:FvA4bwjKp github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554/go.mod h1:n73K/hcuJ50MiVznXyN4rde6fZY7naGKWBXOLFTyc94= github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU= github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= +github.com/pandatix/go-cvss v0.6.2 h1:TFiHlzUkT67s6UkelHmK6s1INKVUG7nlKYiWWDTITGI= +github.com/pandatix/go-cvss v0.6.2/go.mod h1:jDXYlQBZrc8nvrMUVVvTG8PhmuShOnKrxP53nOFkt8Q= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM= @@ -2184,8 +2182,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/client/config.go b/internal/client/config.go index bfe02f3a..de27f66c 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -6,12 +6,14 @@ package client import ( "errors" "fmt" + "log/slog" "net/http" "net/url" "strings" "sync" "time" + "github.com/derailed/k9s/internal/slogs" "k8s.io/cli-runtime/pkg/genericclioptions" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -19,7 +21,8 @@ import ( ) const ( - defaultCallTimeoutDuration = 120 * time.Second + // DefaultCallTimeoutDuration is the default api server call timeout duration. + DefaultCallTimeoutDuration time.Duration = 15 * time.Second // UsePersistentConfig caches client config to avoid reloads. UsePersistentConfig = true @@ -42,12 +45,13 @@ func NewConfig(f *genericclioptions.ConfigFlags) *Config { // CallTimeout returns the call timeout if set or the default if not set. func (c *Config) CallTimeout() time.Duration { if !isSet(c.flags.Timeout) { - return defaultCallTimeoutDuration + return DefaultCallTimeoutDuration } dur, err := time.ParseDuration(*c.flags.Timeout) if err != nil { - return defaultCallTimeoutDuration + return DefaultCallTimeoutDuration } + slog.Debug("APIServer timeout", slogs.Duration, dur) return dur } @@ -95,6 +99,8 @@ func (c *Config) SwitchContext(name string) error { flags.ImpersonateGroup = c.flags.ImpersonateGroup flags.ImpersonateUID = c.flags.ImpersonateUID flags.Insecure = c.flags.Insecure + flags.BearerToken = c.flags.BearerToken + c.flags = flags return nil diff --git a/internal/client/config_test.go b/internal/client/config_test.go index f0d7d72a..c99a24f9 100644 --- a/internal/client/config_test.go +++ b/internal/client/config_test.go @@ -32,7 +32,7 @@ func TestCallTimeout(t *testing.T) { e: 1 * time.Minute, }, "default": { - e: 10 * time.Second, + e: 15 * time.Second, }, } diff --git a/internal/client/gvr.go b/internal/client/gvr.go index 62c2eb6d..21ef9896 100644 --- a/internal/client/gvr.go +++ b/internal/client/gvr.go @@ -17,7 +17,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) -var NoGVR = &GVR{} +var NoGVR = new(GVR) // GVR represents a kubernetes resource schema as a string. // Format is group/version/resources:subresource. diff --git a/internal/client/gvrs.go b/internal/client/gvrs.go index eae47ad7..34b6709a 100644 --- a/internal/client/gvrs.go +++ b/internal/client/gvrs.go @@ -34,7 +34,7 @@ var ( ScGVR = NewGVR("storage.k8s.io/v1/storageclasses") // Policy... - PdbGVR = NewGVR("policy/v1/PodDisruptionBudgets") + PdbGVR = NewGVR("policy/v1/poddisruptionbudgets") PspGVR = NewGVR("policy/v1beta1/podsecuritypolicies") // Metrics... diff --git a/internal/config/config.go b/internal/config/config.go index 1a276a61..06825f05 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,6 +9,7 @@ import ( "io/fs" "log/slog" "os" + "time" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config/data" @@ -79,11 +80,24 @@ func (c *Config) ContextPluginsPath() (string, error) { return AppContextPluginsFile(ct.GetClusterName(), c.K9s.activeContextName), nil } +func setK8sTimeout(flags *genericclioptions.ConfigFlags, d time.Duration) { + v := d.String() + flags.Timeout = &v +} + // Refine the configuration based on cli args. func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, cfg *client.Config) error { if flags == nil { return nil } + + if !isStringSet(flags.Timeout) { + if d, err := time.ParseDuration(c.K9s.APIServerTimeout); err == nil { + setK8sTimeout(flags, d) + } else { + setK8sTimeout(flags, client.DefaultCallTimeoutDuration) + } + } if isStringSet(flags.Context) { if _, err := c.K9s.ActivateContext(*flags.Context); err != nil { return fmt.Errorf("k8sflags. unable to activate context %q: %w", *flags.Context, err) @@ -107,6 +121,7 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c c.ResetActiveView() case isStringSet(flags.Namespace): ns = *flags.Namespace + c.ResetActiveView() default: nss, err := c.K9s.ActiveContextNamespace() if err != nil { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 37f5ee0b..f4504a0c 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -49,9 +49,8 @@ func TestConfigSave(t *testing.T) { }, } - for k := range uu { + for k, u := range uu { xdg.Reload() - u := uu[k] t.Run(k, func(t *testing.T) { c := mock.NewMockConfig(t) _, err := c.K9s.ActivateContext(u.ct) @@ -562,6 +561,7 @@ func TestConfigSaveFile(t *testing.T) { require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true)) cfg.K9s.RefreshRate = 100 + cfg.K9s.APIServerTimeout = "30s" cfg.K9s.ReadOnly = true cfg.K9s.Logger.TailCount = 500 cfg.K9s.Logger.BufferSize = 800 diff --git a/internal/config/json/schemas/k9s.json b/internal/config/json/schemas/k9s.json index ebc2a5ec..e19099a3 100644 --- a/internal/config/json/schemas/k9s.json +++ b/internal/config/json/schemas/k9s.json @@ -10,6 +10,7 @@ "liveViewAutoRefresh": { "type": "boolean" }, "screenDumpDir": {"type": "string"}, "refreshRate": { "type": "integer" }, + "apiServerTimeout": { "type": "string" }, "maxConnRetry": { "type": "integer" }, "readOnly": { "type": "boolean" }, "noExitOnCtrlC": { "type": "boolean" }, diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 462f999c..92536810 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -24,6 +24,7 @@ type K9s struct { LiveViewAutoRefresh bool `json:"liveViewAutoRefresh" yaml:"liveViewAutoRefresh"` ScreenDumpDir string `json:"screenDumpDir" yaml:"screenDumpDir,omitempty"` RefreshRate int `json:"refreshRate" yaml:"refreshRate"` + APIServerTimeout string `json:"apiServerTimeout" yaml:"apiServerTimeout"` MaxConnRetry int32 `json:"maxConnRetry" yaml:"maxConnRetry"` ReadOnly bool `json:"readOnly" yaml:"readOnly"` NoExitOnCtrlC bool `json:"noExitOnCtrlC" yaml:"noExitOnCtrlC"` @@ -54,6 +55,7 @@ func NewK9s(conn client.Connection, ks data.KubeSettings) *K9s { return &K9s{ RefreshRate: defaultRefreshRate, MaxConnRetry: defaultMaxConnRetry, + APIServerTimeout: client.DefaultCallTimeoutDuration.String(), ScreenDumpDir: AppDumpsDir, Logger: NewLogger(), Thresholds: NewThreshold(), @@ -117,9 +119,11 @@ func (k *K9s) Merge(k1 *K9s) { k.DefaultView = k1.DefaultView k.ScreenDumpDir = k1.ScreenDumpDir k.RefreshRate = k1.RefreshRate + k.APIServerTimeout = k1.APIServerTimeout k.MaxConnRetry = k1.MaxConnRetry k.ReadOnly = k1.ReadOnly k.NoExitOnCtrlC = k1.NoExitOnCtrlC + k.PortForwardAddress = k1.PortForwardAddress k.UI = k1.UI k.SkipLatestRevCheck = k1.SkipLatestRevCheck k.DisablePodCounting = k1.DisablePodCounting diff --git a/internal/config/logger.go b/internal/config/logger.go index 7446c418..5087f82f 100644 --- a/internal/config/logger.go +++ b/internal/config/logger.go @@ -8,7 +8,7 @@ const ( DefaultLoggerTailCount = 100 // MaxLogThreshold sets the max value for log size. - MaxLogThreshold = 5000 + MaxLogThreshold = 5_000 // DefaultSinceSeconds tracks default log age. DefaultSinceSeconds = -1 // tail logs by default diff --git a/internal/config/testdata/configs/default.yaml b/internal/config/testdata/configs/default.yaml index feb8b77a..f4b21aaf 100644 --- a/internal/config/testdata/configs/default.yaml +++ b/internal/config/testdata/configs/default.yaml @@ -2,6 +2,7 @@ k9s: liveViewAutoRefresh: false screenDumpDir: /tmp/k9s-test/screen-dumps refreshRate: 2 + apiServerTimeout: 15s maxConnRetry: 5 readOnly: false noExitOnCtrlC: false diff --git a/internal/config/testdata/configs/expected.yaml b/internal/config/testdata/configs/expected.yaml index 0fd8085a..51cded39 100644 --- a/internal/config/testdata/configs/expected.yaml +++ b/internal/config/testdata/configs/expected.yaml @@ -2,6 +2,7 @@ k9s: liveViewAutoRefresh: true screenDumpDir: /tmp/k9s-test/screen-dumps refreshRate: 100 + apiServerTimeout: 30s maxConnRetry: 5 readOnly: true noExitOnCtrlC: false diff --git a/internal/config/testdata/configs/k9s.yaml b/internal/config/testdata/configs/k9s.yaml index 053fa9bf..c8c23aa6 100644 --- a/internal/config/testdata/configs/k9s.yaml +++ b/internal/config/testdata/configs/k9s.yaml @@ -2,6 +2,7 @@ k9s: liveViewAutoRefresh: true screenDumpDir: /tmp/k9s-test/screen-dumps refreshRate: 2 + apiServerTimeout: 10s maxConnRetry: 5 readOnly: false noExitOnCtrlC: false diff --git a/internal/dao/dp.go b/internal/dao/dp.go index 57b4aa52..9ccdc2dd 100644 --- a/internal/dao/dp.go +++ b/internal/dao/dp.go @@ -56,8 +56,8 @@ func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) err } // Restart a Deployment rollout. -func (d *Deployment) Restart(ctx context.Context, path string) error { - return restartRes[*appsv1.Deployment](ctx, d.getFactory(), client.DpGVR, path) +func (d *Deployment) Restart(ctx context.Context, path string, opts *metav1.PatchOptions) error { + return restartRes[*appsv1.Deployment](ctx, d.getFactory(), client.DpGVR, path, opts) } // TailLogs tail logs for all pods represented by this Deployment. @@ -395,7 +395,7 @@ func scaleRes(ctx context.Context, f Factory, gvr *client.GVR, path string, repl } } -func restartRes[T runtime.Object](ctx context.Context, f Factory, gvr *client.GVR, path string) error { +func restartRes[T runtime.Object](ctx context.Context, f Factory, gvr *client.GVR, path string, opts *metav1.PatchOptions) error { o, err := f.Get(gvr, path, true, labels.Everything()) if err != nil { return err @@ -440,7 +440,7 @@ func restartRes[T runtime.Object](ctx context.Context, f Factory, gvr *client.GV n, types.StrategicMergePatchType, diff, - metav1.PatchOptions{}, + *opts, ) case client.DsGVR: @@ -449,7 +449,7 @@ func restartRes[T runtime.Object](ctx context.Context, f Factory, gvr *client.GV n, types.StrategicMergePatchType, diff, - metav1.PatchOptions{}, + *opts, ) case client.StsGVR: @@ -458,7 +458,7 @@ func restartRes[T runtime.Object](ctx context.Context, f Factory, gvr *client.GV n, types.StrategicMergePatchType, diff, - metav1.PatchOptions{}, + *opts, ) } diff --git a/internal/dao/ds.go b/internal/dao/ds.go index 1e3a7c25..409220dc 100644 --- a/internal/dao/ds.go +++ b/internal/dao/ds.go @@ -50,8 +50,8 @@ func (d *DaemonSet) ListImages(_ context.Context, fqn string) ([]string, error) } // Restart a DaemonSet rollout. -func (d *DaemonSet) Restart(ctx context.Context, path string) error { - return restartRes[*appsv1.DaemonSet](ctx, d.getFactory(), client.DsGVR, path) +func (d *DaemonSet) Restart(ctx context.Context, path string, opts *metav1.PatchOptions) error { + return restartRes[*appsv1.DaemonSet](ctx, d.getFactory(), client.DsGVR, path, opts) } // TailLogs tail logs for all pods represented by this DaemonSet. diff --git a/internal/dao/dynamic.go b/internal/dao/dynamic.go index 9e2293b3..34b2569e 100644 --- a/internal/dao/dynamic.go +++ b/internal/dao/dynamic.go @@ -13,6 +13,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" @@ -39,7 +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) + sel := labels.Everything() + if s, ok := ctx.Value(internal.KeyLabels).(labels.Selector); ok { + sel = s + } + opts := []string{d.gvr.AsResourceName()} ns, n := client.Namespaced(fqn) if n != "" { @@ -51,7 +56,7 @@ func (d *Dynamic) toTable(ctx context.Context, fqn string) ([]runtime.Object, er b := f.NewBuilder(). Unstructured(). NamespaceParam(ns).DefaultNamespace().AllNamespaces(allNS). - LabelSelectorParam(strLabel). + LabelSelectorParam(sel.String()). FieldSelectorParam(""). RequestChunksOf(0). ResourceTypeOrNameArgs(true, opts...). diff --git a/internal/dao/generic.go b/internal/dao/generic.go index c5c4f6ff..d7ee6b9d 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -11,6 +11,7 @@ import ( "github.com/derailed/k9s/internal/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/dynamic" ) @@ -38,7 +39,10 @@ type Generic struct { // List returns a collection of resources. // BOZO!! no auth check?? func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) { - labelSel, _ := ctx.Value(internal.KeyLabels).(string) + labelSel, ok := ctx.Value(internal.KeyLabels).(labels.Selector) + if !ok { + labelSel = labels.Everything() + } if client.IsAllNamespace(ns) { ns = client.BlankNamespace } @@ -48,11 +52,12 @@ func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) return nil, err } + opts := metav1.ListOptions{LabelSelector: labelSel.String()} var ll *unstructured.UnstructuredList if client.IsClusterScoped(ns) { - ll, err = dial.List(ctx, metav1.ListOptions{LabelSelector: labelSel}) + ll, err = dial.List(ctx, opts) } else { - ll, err = dial.Namespace(ns).List(ctx, metav1.ListOptions{LabelSelector: labelSel}) + ll, err = dial.Namespace(ns).List(ctx, opts) } if err != nil { return nil, err diff --git a/internal/dao/resource.go b/internal/dao/resource.go index ea7a4695..39fee900 100644 --- a/internal/dao/resource.go +++ b/internal/dao/resource.go @@ -25,12 +25,9 @@ type Resource struct { // List returns a collection of resources. func (r *Resource) List(ctx context.Context, ns string) ([]runtime.Object, error) { - strLabel, _ := ctx.Value(internal.KeyLabels).(string) lsel := labels.Everything() - if strLabel != "" { - if sel, err := labels.Parse(strLabel); err == nil { - lsel = sel - } + if sel, ok := ctx.Value(internal.KeyLabels).(labels.Selector); ok { + lsel = sel } return r.getFactory().List(r.gvr, ns, false, lsel) diff --git a/internal/dao/sts.go b/internal/dao/sts.go index c6f7d9f0..e774f68b 100644 --- a/internal/dao/sts.go +++ b/internal/dao/sts.go @@ -54,8 +54,8 @@ func (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) er } // Restart a StatefulSet rollout. -func (s *StatefulSet) Restart(ctx context.Context, path string) error { - return restartRes[*appsv1.StatefulSet](ctx, s.getFactory(), client.StsGVR, path) +func (s *StatefulSet) Restart(ctx context.Context, path string, opts *metav1.PatchOptions) error { + return restartRes[*appsv1.StatefulSet](ctx, s.getFactory(), client.StsGVR, path, opts) } // GetInstance returns a statefulset instance. diff --git a/internal/dao/table.go b/internal/dao/table.go index 7b0240fe..a5b4cdd4 100644 --- a/internal/dao/table.go +++ b/internal/dao/table.go @@ -12,6 +12,7 @@ import ( "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/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/rest" @@ -56,7 +57,10 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { // List all Resources in a given namespace. func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { - labelSel, _ := ctx.Value(internal.KeyLabels).(string) + sel := labels.Everything() + if labelSel, ok := ctx.Value(internal.KeyLabels).(labels.Selector); ok { + sel = labelSel + } fieldSel, _ := ctx.Value(internal.KeyFields).(string) includeObject := includeMeta @@ -75,7 +79,7 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { Namespace(ns). Resource(t.gvr.R()). VersionedParams(&metav1.ListOptions{ - LabelSelector: labelSel, + LabelSelector: sel.String(), FieldSelector: fieldSel, }, metav1.ParameterCodec). Do(ctx).Get() diff --git a/internal/dao/types.go b/internal/dao/types.go index 08f1d906..bb149ff8 100644 --- a/internal/dao/types.go +++ b/internal/dao/types.go @@ -145,7 +145,7 @@ type Switchable interface { // Restartable represents a restartable resource. type Restartable interface { // Restart performs a rollout restart. - Restart(ctx context.Context, path string) error + Restart(context.Context, string, *metav1.PatchOptions) error } // Runnable represents a runnable resource. diff --git a/internal/model/stack_test.go b/internal/model/stack_test.go index 15a21d85..6926a152 100644 --- a/internal/model/stack_test.go +++ b/internal/model/stack_test.go @@ -13,6 +13,7 @@ import ( "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/labels" ) func init() { @@ -313,4 +314,4 @@ func (c) Start() {} func (c) Stop() {} func (c) Init(context.Context) error { return nil } func (c) SetFilter(string) {} -func (c) SetLabelFilter(map[string]string) {} +func (c) SetLabelSelector(labels.Selector) {} diff --git a/internal/model/table.go b/internal/model/table.go index 5283c401..cbb5b227 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -19,6 +19,7 @@ import ( "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/slogs" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ) @@ -38,15 +39,15 @@ type TableListener interface { // Table represents a table model. type Table struct { - gvr *client.GVR - data *model1.TableData - listeners []TableListener - inUpdate int32 - refreshRate time.Duration - instance string - labelFilter string - mx sync.RWMutex - vs *config.ViewSetting + gvr *client.GVR + data *model1.TableData + listeners []TableListener + inUpdate int32 + refreshRate time.Duration + instance string + labelSelector labels.Selector + mx sync.RWMutex + vs *config.ViewSetting } // NewTable returns a new table model. @@ -70,20 +71,20 @@ func (t *Table) SetViewSetting(ctx context.Context, vs *config.ViewSetting) { } } -// SetLabelFilter sets the labels filter. -func (t *Table) SetLabelFilter(f string) { +// SetLabelSelector sets the labels selector. +func (t *Table) SetLabelSelector(sel labels.Selector) { t.mx.Lock() defer t.mx.Unlock() - t.labelFilter = f + t.labelSelector = sel } -// GetLabelFilter sets the labels filter. -func (t *Table) GetLabelFilter() string { +// GetLabelSelector sets the labels selector. +func (t *Table) GetLabelSelector() labels.Selector { t.mx.Lock() defer t.mx.Unlock() - return t.labelFilter + return t.labelSelector } // SetInstance sets a single entry table. @@ -253,7 +254,7 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err a.Init(factory, t.gvr) t.mx.RLock() - ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) + ctx = context.WithValue(ctx, internal.KeyLabels, t.labelSelector) t.mx.RUnlock() ns := client.CleanseNamespace(t.data.GetNamespace()) @@ -273,7 +274,7 @@ func (t *Table) reconcile(ctx context.Context) error { if t.vs != nil { meta.DAO.SetIncludeObject(true) } - ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) + ctx = context.WithValue(ctx, internal.KeyLabels, t.labelSelector) if t.instance == "" { oo, err = t.list(ctx, meta.DAO) } else { diff --git a/internal/model/types.go b/internal/model/types.go index 9d7d9225..6f496fc0 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -13,6 +13,7 @@ import ( "github.com/derailed/k9s/internal/view/cmd" "github.com/derailed/tview" "github.com/sahilm/fuzzy" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ) @@ -105,7 +106,7 @@ type Viewer interface { type Filterer interface { SetFilter(string) - SetLabelFilter(map[string]string) + SetLabelSelector(labels.Selector) } // Cruder performs crud operations. diff --git a/internal/model1/header.go b/internal/model1/header.go index 4355d2b5..9fe79be7 100644 --- a/internal/model1/header.go +++ b/internal/model1/header.go @@ -8,7 +8,9 @@ import ( "log/slog" "reflect" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/slogs" + "k8s.io/apimachinery/pkg/util/sets" ) const ageCol = "AGE" @@ -171,6 +173,24 @@ func (h Header) Diff(header Header) bool { return !reflect.DeepEqual(h, header) } +// FilterColIndices return viewable col header indices. +func (h Header) FilterColIndices(ns string, wide bool) sets.Set[int] { + if len(h) == 0 { + return nil + } + nsed := client.IsNamespaced(ns) + + cc := sets.New[int]() + for i, c := range h { + if c.Name == "AGE" || !wide && c.Wide || c.Hide || (nsed && c.Name == "NAMESPACE") { + continue + } + cc.Insert(i) + } + + return cc +} + // ColumnNames return header col names func (h Header) ColumnNames(wide bool) []string { if len(h) == 0 { diff --git a/internal/model1/table_data.go b/internal/model1/table_data.go index f622b9ac..8b6c516f 100644 --- a/internal/model1/table_data.go +++ b/internal/model1/table_data.go @@ -164,6 +164,10 @@ func (t *TableData) Filter(f FilterOpts) *TableData { } func (t *TableData) rxFilter(q string, inverse bool) (*RowEvents, error) { + if strings.Contains(q, " ") { + return t.rowEvents, nil + } + if inverse { q = q[1:] } @@ -172,20 +176,16 @@ func (t *TableData) rxFilter(q string, inverse bool) (*RowEvents, error) { return nil, fmt.Errorf("invalid rx filter %q: %w", q, err) } - var startIndex int - if _, ok := t.header.IndexOf("NAMESPACE", true); ok && client.IsNamespaced(t.namespace) { - startIndex = 1 - } + vidx := t.header.FilterColIndices(t.namespace, true) rr := NewRowEvents(t.RowCount() / 2) - ageIndex, _ := t.header.IndexOf("AGE", true) t.rowEvents.Range(func(_ int, re RowEvent) bool { ff := make([]string, 0, len(re.Row.Fields)) - for _, r := range re.Row.Fields[startIndex:] { + for idx, r := range re.Row.Fields { + if !vidx.Has(idx) { + continue + } ff = append(ff, r) } - if ageIndex >= 0 && startIndex != ageIndex && ageIndex+1 <= len(ff) { - ff = append(ff[0:ageIndex], ff[ageIndex+1:]...) - } match := rx.MatchString(strings.Join(ff, spacer)) if (inverse && !match) || (!inverse && match) { rr.Add(re) diff --git a/internal/render/dp.go b/internal/render/dp.go index 845f9c13..ed6cdba0 100644 --- a/internal/render/dp.go +++ b/internal/render/dp.go @@ -87,16 +87,20 @@ func (d Deployment) defaultRow(raw *unstructured.Unstructured, r *model1.Row) er return err } + var desired int32 + if dp.Spec.Replicas != nil { + desired = *dp.Spec.Replicas + } r.ID = client.MetaFQN(&dp.ObjectMeta) r.Fields = model1.Fields{ dp.Namespace, dp.Name, 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.AvailableReplicas)) + "/" + strconv.Itoa(int(desired)), strconv.Itoa(int(dp.Status.UpdatedReplicas)), strconv.Itoa(int(dp.Status.AvailableReplicas)), mapToStr(dp.Labels), - AsStatus(d.diagnose(dp.Status.Replicas, dp.Status.AvailableReplicas)), + AsStatus(d.diagnose(desired, dp.Status.AvailableReplicas)), ToAge(dp.GetCreationTimestamp()), } diff --git a/internal/slogs/keys.go b/internal/slogs/keys.go index 9fb007d9..ac0ea9ab 100644 --- a/internal/slogs/keys.go +++ b/internal/slogs/keys.go @@ -215,4 +215,7 @@ const ( // JQExp tracks a jq expression logger key. JQExp = "jq-exp" + + // Duration tracks a duration logger key. + Duration = "duration" ) diff --git a/internal/ui/crumbs_test.go b/internal/ui/crumbs_test.go index 508e513d..0bb84b02 100644 --- a/internal/ui/crumbs_test.go +++ b/internal/ui/crumbs_test.go @@ -15,6 +15,7 @@ import ( "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/labels" ) func init() { @@ -60,4 +61,4 @@ func (c) Start() {} func (c) Stop() {} func (c) Init(context.Context) error { return nil } func (c) SetFilter(string) {} -func (c) SetLabelFilter(map[string]string) {} +func (c) SetLabelSelector(labels.Selector) {} diff --git a/internal/ui/dialog/restart.go b/internal/ui/dialog/restart.go new file mode 100644 index 00000000..fa40e3c1 --- /dev/null +++ b/internal/ui/dialog/restart.go @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dialog + +import ( + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type RestartFn func(*metav1.PatchOptions) bool + +type RestartDialogOpts struct { + Title, Message string + FieldManager string + Ack RestartFn + Cancel cancelFunc +} + +func ShowRestart(styles *config.Dialog, pages *ui.Pages, opts *RestartDialogOpts) { + f := tview.NewForm() + f.SetItemPadding(0) + f.SetButtonsAlign(tview.AlignCenter). + SetButtonBackgroundColor(styles.ButtonBgColor.Color()). + SetButtonTextColor(styles.ButtonFgColor.Color()). + SetLabelColor(styles.LabelFgColor.Color()). + SetFieldTextColor(styles.FieldFgColor.Color()) + f.AddButton("Cancel", func() { + dismissConfirm(pages) + opts.Cancel() + }) + + modal := tview.NewModalForm("<"+opts.Title+">", f) + + args := metav1.PatchOptions{ + FieldManager: opts.FieldManager, + } + f.AddInputField("FieldManager:", args.FieldManager, 40, nil, func(v string) { + args.FieldManager = v + }) + + f.AddButton("OK", func() { + if !opts.Ack(&args) { + return + } + dismissConfirm(pages) + opts.Cancel() + }) + for i := range 2 { + b := f.GetButton(i) + if b == nil { + continue + } + b.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()) + b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color()) + } + f.SetFocus(0) + + message := opts.Message + modal.SetText(message) + modal.SetTextColor(styles.FgColor.Color()) + modal.SetDoneFunc(func(int, string) { + dismissConfirm(pages) + opts.Cancel() + }) + pages.AddPage(confirmKey, modal, false, false) + pages.ShowPage(confirmKey) +} diff --git a/internal/ui/table.go b/internal/ui/table.go index ac515007..2b1931c5 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -315,12 +315,18 @@ func (t *Table) doUpdate(data *model1.TableData) *model1.TableData { } else { t.actions.Delete(KeyShiftP) } - t.setSortCol(data.ComputeSortCol(t.GetViewSetting(), t.getSortCol(), t.getMSort())) return data } +func (t *Table) shouldExcludeColumn(h model1.HeaderColumn) bool { + return (h.Hide || (!t.wide && h.Wide)) || + (h.Name == "NAMESPACE" && !t.GetModel().ClusterWide()) || + (h.MX && !t.hasMetrics) || + (h.VS && vul.ImgScanner == nil) +} + func (t *Table) UpdateUI(cdata, data *model1.TableData) { t.Clear() fg := t.styles.Table().Header.FgColor.Color() @@ -328,19 +334,9 @@ func (t *Table) UpdateUI(cdata, data *model1.TableData) { var col int for _, h := range cdata.Header() { - if h.Hide || (!t.wide && h.Wide) { + if t.shouldExcludeColumn(h) { continue } - if h.Name == "NAMESPACE" && !t.GetModel().ClusterWide() { - continue - } - if h.MX && !t.hasMetrics { - continue - } - if h.VS && vul.ImgScanner == nil { - continue - } - t.AddHeaderCell(col, h) c := t.GetCell(0, col) c.SetBackgroundColor(bg) @@ -384,17 +380,7 @@ func (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads M ) continue } - if h[c].Hide || (!t.wide && h[c].Wide) { - continue - } - - if h[c].Name == "NAMESPACE" && !t.GetModel().ClusterWide() { - continue - } - if h[c].MX && !t.hasMetrics { - continue - } - if h[c].VS && vul.ImgScanner == nil { + if t.shouldExcludeColumn(h[c]) { continue } @@ -468,7 +454,6 @@ func (t *Table) Refresh() { if data.HeaderCount() == 0 { return } - // BOZO!! Really want to tell model reload now. Refactor! cdata := t.Update(data, t.hasMetrics) t.UpdateUI(cdata, data) } @@ -575,9 +560,14 @@ func (t *Table) styleTitle() string { buff := t.cmdBuff.GetText() if internal.IsLabelSelector(buff) { - buff = render.Truncate(TrimLabelSelector(buff), maxTruncate) - } else if l := t.GetModel().GetLabelFilter(); l != "" { - buff = render.Truncate(l, maxTruncate) + sel, err := TrimLabelSelector(buff) + if err != nil { + buff = render.Truncate(sel.String(), maxTruncate) + } + } else if l := t.GetModel().GetLabelSelector(); l != nil && !l.Empty() { + buff = render.Truncate(l.String(), maxTruncate) + } else if buff != "" { + buff = render.Truncate(buff, maxTruncate) } if buff == "" { diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go index 9c783cac..6e66d9a0 100644 --- a/internal/ui/table_helper.go +++ b/internal/ui/table_helper.go @@ -13,6 +13,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/slogs" + "k8s.io/apimachinery/pkg/labels" ) const ( @@ -58,12 +59,13 @@ func TrimCell(tv *SelectTable, row, col int) string { } // TrimLabelSelector extracts label query. -func TrimLabelSelector(s string) string { +func TrimLabelSelector(s string) (labels.Selector, error) { + var selStr string if strings.Index(s, "-l") == 0 { - return strings.TrimSpace(s[2:]) + selStr = strings.TrimSpace(s[2:]) } - return s + return labels.Parse(selStr) } // SkinTitle decorates a title. diff --git a/internal/ui/table_helper_test.go b/internal/ui/table_helper_test.go index 7bec2d40..48c8c274 100644 --- a/internal/ui/table_helper_test.go +++ b/internal/ui/table_helper_test.go @@ -8,6 +8,7 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/labels" ) func TestTruncate(t *testing.T) { @@ -34,17 +35,29 @@ func TestTruncate(t *testing.T) { } func TestTrimLabelSelector(t *testing.T) { + sel, _ := labels.Parse("app=fred,env=blee") uu := map[string]struct { - sel, e string + sel string + err error + e labels.Selector }{ - "cool": {"-l app=fred,env=blee", "app=fred,env=blee"}, - "noSpace": {"-lapp=fred,env=blee", "app=fred,env=blee"}, + "cool": { + sel: "-l app=fred,env=blee", + e: sel, + }, + + "no-space": { + sel: "-lapp=fred,env=blee", + e: sel, + }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, TrimLabelSelector(u.sel)) + sel, err := TrimLabelSelector(u.sel) + assert.Equal(t, u.err, err) + assert.Equal(t, u.e, sel) }) } } diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index 1c86d459..df07fb3f 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -17,6 +17,7 @@ import ( "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ) @@ -42,7 +43,7 @@ func TestTableUpdate(t *testing.T) { func TestTableSelection(t *testing.T) { v := ui.NewTable(client.NewGVR("fred")) v.Init(makeContext()) - m := &mockModel{} + m := new(mockModel) v.SetModel(m) data := m.Peek() cdata := v.Update(data, false) @@ -72,8 +73,8 @@ var _ ui.Tabular = &mockModel{} func (*mockModel) SetViewSetting(context.Context, *config.ViewSetting) {} func (*mockModel) SetInstance(string) {} -func (*mockModel) SetLabelFilter(string) {} -func (*mockModel) GetLabelFilter() string { return "" } +func (*mockModel) SetLabelSelector(labels.Selector) {} +func (*mockModel) GetLabelSelector() labels.Selector { return nil } func (*mockModel) Empty() bool { return false } func (*mockModel) RowCount() int { return 1 } func (*mockModel) HasMetrics() bool { return true } diff --git a/internal/ui/types.go b/internal/ui/types.go index bfc1944c..24c20e51 100644 --- a/internal/ui/types.go +++ b/internal/ui/types.go @@ -12,6 +12,7 @@ import ( "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ) @@ -49,11 +50,11 @@ type Tabular interface { // SetInstance sets parent resource path. SetInstance(string) - // SetLabelFilter sets the label filter. - SetLabelFilter(string) + // SetLabelSelector sets the label selector. + SetLabelSelector(labels.Selector) - // GetLabelFilter fetch the label filter. - GetLabelFilter() string + // GetLabelSelector fetch the label filter. + GetLabelSelector() labels.Selector // Empty returns true if model has no data. Empty() bool diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go index 09a959ad..04d145ae 100644 --- a/internal/view/alias_test.go +++ b/internal/view/alias_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ) @@ -35,7 +36,7 @@ func TestAliasNew(t *testing.T) { func TestAliasSearch(t *testing.T) { v := view.NewAlias(client.AliGVR) require.NoError(t, v.Init(makeContext(t))) - v.GetTable().SetModel(&mockModel{}) + v.GetTable().SetModel(new(mockModel)) v.GetTable().Refresh() v.App().Prompt().SetModel(v.GetTable().CmdBuff()) v.App().Prompt().SendStrokes("blee") @@ -93,8 +94,8 @@ func (*mockModel) NextSuggestion() (string, bool) { return 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) SetLabelSelector(labels.Selector) {} +func (*mockModel) GetLabelSelector() labels.Selector { return nil } func (*mockModel) Empty() bool { return false } func (*mockModel) RowCount() int { return 1 } func (*mockModel) HasMetrics() bool { return true } diff --git a/internal/view/browser.go b/internal/view/browser.go index 3e3b044a..6b25be6d 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -26,6 +26,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/labels" "k8s.io/apimachinery/pkg/util/sets" ) @@ -194,9 +195,11 @@ func (b *Browser) SetFilter(s string) { b.CmdBuff().SetText(s, "") } -func (b *Browser) SetLabelFilter(labels map[string]string) { - b.CmdBuff().SetText(toLabelsStr(labels), "") - b.GetModel().SetLabelFilter(toLabelsStr(labels)) +func (b *Browser) SetLabelSelector(sel labels.Selector) { + if sel != nil { + b.CmdBuff().SetText(sel.String(), "") + } + b.GetModel().SetLabelSelector(sel) } // BufferChanged indicates the buffer was changed. @@ -205,9 +208,11 @@ func (*Browser) BufferChanged(_, _ string) {} // BufferCompleted indicates input was accepted. func (b *Browser) BufferCompleted(text, _ string) { if internal.IsLabelSelector(text) { - b.GetModel().SetLabelFilter(ui.TrimLabelSelector(text)) + if sel, err := ui.TrimLabelSelector(text); err == nil { + b.GetModel().SetLabelSelector(sel) + } } else { - b.GetModel().SetLabelFilter("") + b.GetModel().SetLabelSelector(labels.Everything()) } } @@ -375,7 +380,7 @@ func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey { hasFilter := !b.CmdBuff().Empty() b.CmdBuff().ClearText(false) if hasFilter { - b.GetModel().SetLabelFilter("") + b.GetModel().SetLabelSelector(labels.Everything()) b.Refresh() } return b.App().PrevCmd(evt) @@ -555,7 +560,9 @@ func (b *Browser) defaultContext() context.Context { ctx = context.WithValue(ctx, internal.KeyGVR, b.GVR()) ctx = context.WithValue(ctx, internal.KeyPath, b.Path) if internal.IsLabelSelector(b.CmdBuff().GetText()) { - ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(b.CmdBuff().GetText())) + if sel, err := ui.TrimLabelSelector(b.CmdBuff().GetText()); err == nil { + ctx = context.WithValue(ctx, internal.KeyLabels, sel) + } } ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(b.App().Config.ActiveNamespace())) ctx = context.WithValue(ctx, internal.KeyWithMetrics, b.app.factory.Client().HasMetrics()) diff --git a/internal/view/command.go b/internal/view/command.go index ba250549..e910141f 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -16,6 +16,7 @@ import ( "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/slogs" "github.com/derailed/k9s/internal/view/cmd" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/sets" ) @@ -199,15 +200,15 @@ func (c *Command) run(p *cmd.Interpreter, fqn string, clearStack, pushCmd bool) co := c.componentFor(gvr, fqn, v) co.SetFilter("") - co.SetLabelFilter(nil) + co.SetLabelSelector(labels.Everything()) if f, ok := p.FilterArg(); ok { co.SetFilter(f) } if f, ok := p.FuzzyArg(); ok { co.SetFilter("-f " + f) } - if ll, ok := p.LabelsArg(); ok { - co.SetLabelFilter(ll) + if ss, ok := p.LabelsArg(); ok { + co.SetLabelSelector(labels.SelectorFromSet(ss)) } return c.exec(p, gvr, co, clearStack, pushCmd) diff --git a/internal/view/details.go b/internal/view/details.go index f785db74..ef1c79c1 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -16,6 +16,7 @@ import ( "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/sahilm/fuzzy" + "k8s.io/apimachinery/pkg/labels" ) const ( @@ -61,7 +62,7 @@ func NewDetails(app *App, title, subject, contentType string, searchable bool) * func (*Details) SetCommand(*cmd.Interpreter) {} func (*Details) SetFilter(string) {} -func (*Details) SetLabelFilter(map[string]string) {} +func (*Details) SetLabelSelector(labels.Selector) {} // Init initializes the viewer. func (d *Details) Init(_ context.Context) error { diff --git a/internal/view/dp.go b/internal/view/dp.go index 02f80076..60b8727a 100644 --- a/internal/view/dp.go +++ b/internal/view/dp.go @@ -90,5 +90,5 @@ func showPodsFromSelector(app *App, path string, sel *metav1.LabelSelector) { return } - showPods(app, path, l.String(), "") + showPods(app, path, l, "") } diff --git a/internal/view/help.go b/internal/view/help.go index e610c074..77cda9ba 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -17,6 +17,7 @@ import ( "github.com/derailed/k9s/internal/view/cmd" "github.com/derailed/tcell/v2" "github.com/derailed/tview" + "k8s.io/apimachinery/pkg/labels" ) const ( @@ -46,7 +47,7 @@ func NewHelp(app *App) *Help { func (*Help) SetCommand(*cmd.Interpreter) {} func (*Help) SetFilter(string) {} -func (*Help) SetLabelFilter(map[string]string) {} +func (*Help) SetLabelSelector(labels.Selector) {} // Init initializes the component. func (h *Help) Init(ctx context.Context) error { diff --git a/internal/view/helpers.go b/internal/view/helpers.go index b53bc52f..de229a9a 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -23,11 +23,11 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/slogs" "github.com/derailed/k9s/internal/ui" - "github.com/derailed/k9s/internal/view/cmd" "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/sahilm/fuzzy" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/sets" ) @@ -135,19 +135,10 @@ func describeResource(app *App, _ ui.Tabular, gvr *client.GVR, path string) { } } -func toLabelsStr(labels map[string]string) string { - ll := make([]string, 0, len(labels)) - for k, v := range labels { - ll = append(ll, fmt.Sprintf("%s=%s", k, v)) - } - - return strings.Join(ll, ",") -} - -func showPods(app *App, path, labelSel, fieldSel string) { +func showPods(app *App, path string, labelSel labels.Selector, fieldSel string) { v := NewPod(client.PodGVR) v.SetContextFn(podCtx(app, path, fieldSel)) - v.SetLabelFilter(cmd.ToLabels(labelSel)) + v.SetLabelSelector(labelSel) ns, _ := client.Namespaced(path) if err := app.Config.SetActiveNamespace(ns); err != nil { diff --git a/internal/view/live_view.go b/internal/view/live_view.go index dcbbd070..bebd6885 100644 --- a/internal/view/live_view.go +++ b/internal/view/live_view.go @@ -19,6 +19,7 @@ import ( "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/sahilm/fuzzy" + "k8s.io/apimachinery/pkg/labels" ) const ( @@ -64,7 +65,7 @@ func NewLiveView(app *App, title string, m model.ResourceViewer) *LiveView { func (*LiveView) SetCommand(*cmd.Interpreter) {} func (*LiveView) SetFilter(string) {} -func (*LiveView) SetLabelFilter(map[string]string) {} +func (*LiveView) SetLabelSelector(labels.Selector) {} // Init initializes the viewer. func (v *LiveView) Init(_ context.Context) error { diff --git a/internal/view/log.go b/internal/view/log.go index 5bd71783..fc5b254f 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -25,6 +25,7 @@ import ( "github.com/derailed/k9s/internal/view/cmd" "github.com/derailed/tcell/v2" "github.com/derailed/tview" + "k8s.io/apimachinery/pkg/labels" ) const ( @@ -66,7 +67,7 @@ func NewLog(gvr *client.GVR, opts *dao.LogOptions) *Log { func (*Log) SetCommand(*cmd.Interpreter) {} func (*Log) SetFilter(string) {} -func (*Log) SetLabelFilter(map[string]string) {} +func (*Log) SetLabelSelector(labels.Selector) {} // Init initializes the viewer. func (l *Log) Init(ctx context.Context) (err error) { diff --git a/internal/view/node.go b/internal/view/node.go index ff184006..9774a288 100644 --- a/internal/view/node.go +++ b/internal/view/node.go @@ -92,7 +92,7 @@ func (n *Node) bindKeys(aa *ui.KeyActions) { } func (n *Node) showPods(a *App, _ ui.Tabular, _ *client.GVR, path string) { - showPods(a, n.GetTable().GetSelectedItem(), client.BlankNamespace, "spec.nodeName="+path) + showPods(a, n.GetTable().GetSelectedItem(), nil, "spec.nodeName="+path) } func (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/picker.go b/internal/view/picker.go index 9abb4b7a..5ff7006d 100644 --- a/internal/view/picker.go +++ b/internal/view/picker.go @@ -11,6 +11,7 @@ import ( "github.com/derailed/k9s/internal/view/cmd" "github.com/derailed/tcell/v2" "github.com/derailed/tview" + "k8s.io/apimachinery/pkg/labels" ) // Picker represents a container picker. @@ -30,7 +31,7 @@ func NewPicker() *Picker { func (*Picker) SetCommand(*cmd.Interpreter) {} func (*Picker) SetFilter(string) {} -func (*Picker) SetLabelFilter(map[string]string) {} +func (*Picker) SetLabelSelector(labels.Selector) {} // Init initializes the view. func (p *Picker) Init(ctx context.Context) error { diff --git a/internal/view/pulse.go b/internal/view/pulse.go index 81c140ad..eb9e43e1 100644 --- a/internal/view/pulse.go +++ b/internal/view/pulse.go @@ -22,6 +22,7 @@ import ( "github.com/derailed/tview" "golang.org/x/text/cases" "golang.org/x/text/language" + "k8s.io/apimachinery/pkg/labels" ) // Graphable represents a graphic component. @@ -80,7 +81,7 @@ func NewPulse(gvr *client.GVR) ResourceViewer { func (*Pulse) SetCommand(*cmd.Interpreter) {} func (*Pulse) SetFilter(string) {} -func (*Pulse) SetLabelFilter(map[string]string) {} +func (*Pulse) SetLabelSelector(labels.Selector) {} // Init initializes the view. func (p *Pulse) Init(ctx context.Context) error { diff --git a/internal/view/restart_extender.go b/internal/view/restart_extender.go index 0ca40297..1c6a9e50 100644 --- a/internal/view/restart_extender.go +++ b/internal/view/restart_extender.go @@ -13,6 +13,7 @@ import ( "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" "github.com/derailed/tcell/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // RestartExtender represents a restartable resource. @@ -54,22 +55,31 @@ func (r *RestartExtender) restartCmd(*tcell.EventKey) *tcell.EventKey { msg = fmt.Sprintf("Restart %d %s?", len(paths), r.GVR().R()) } 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 { - if err := r.restartRollout(ctx, path); err != nil { - r.App().Flash().Err(err) - } else { - r.App().Flash().Infof("Restart in progress for `%s...", path) + + opts := dialog.RestartDialogOpts{ + Title: "Confirm Restart", + Message: msg, + FieldManager: "kubectl-rollout", + Ack: func(opts *metav1.PatchOptions) bool { + ctx, cancel := context.WithTimeout(context.Background(), r.App().Conn().Config().CallTimeout()) + defer cancel() + for _, path := range paths { + if err := r.restartRollout(ctx, path, opts); err != nil { + r.App().Flash().Err(err) + } else { + r.App().Flash().Infof("Restart in progress for `%s...", path) + } } - } - }, func() {}) + return true + }, + Cancel: func() {}, + } + dialog.ShowRestart(&d, r.App().Content.Pages, &opts) return nil } -func (r *RestartExtender) restartRollout(ctx context.Context, path string) error { +func (r *RestartExtender) restartRollout(ctx context.Context, path string, opts *metav1.PatchOptions) error { res, err := dao.AccessorFor(r.App().factory, r.GVR()) if err != nil { return err @@ -79,7 +89,7 @@ func (r *RestartExtender) restartRollout(ctx context.Context, path string) error return errors.New("resource is not restartable") } - return s.Restart(ctx, path) + return s.Restart(ctx, path, opts) } // Helpers... diff --git a/internal/view/svc.go b/internal/view/svc.go index bb233e1f..c46e1eac 100644 --- a/internal/view/svc.go +++ b/internal/view/svc.go @@ -74,7 +74,7 @@ func (s *Service) showPods(a *App, _ ui.Tabular, _ *client.GVR, path string) { return } - showPods(a, path, toLabelsStr(svc.Spec.Selector), "") + showPods(a, path, labels.SelectorFromSet(svc.Spec.Selector), "") } func (*Service) checkSvc(svc *v1.Service) error { diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go index 5d2652e0..3f4f3497 100644 --- a/internal/view/table_int_test.go +++ b/internal/view/table_int_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ) @@ -135,8 +136,8 @@ var _ ui.Tabular = (*mockTableModel)(nil) func (*mockTableModel) SetViewSetting(context.Context, *config.ViewSetting) {} func (*mockTableModel) SetInstance(string) {} -func (*mockTableModel) SetLabelFilter(string) {} -func (*mockTableModel) GetLabelFilter() string { return "" } +func (*mockTableModel) SetLabelSelector(labels.Selector) {} +func (*mockTableModel) GetLabelSelector() labels.Selector { return nil } func (*mockTableModel) Empty() bool { return false } func (*mockTableModel) RowCount() int { return 1 } func (*mockTableModel) HasMetrics() bool { return true } diff --git a/internal/view/workload.go b/internal/view/workload.go index c4f6479f..8c2c18ef 100644 --- a/internal/view/workload.go +++ b/internal/view/workload.go @@ -112,7 +112,9 @@ func (w *Workload) defaultContext(gvr *client.GVR, fqn string) context.Context { ctx = context.WithValue(ctx, internal.KeyPath, fqn) } if internal.IsLabelSelector(w.GetTable().CmdBuff().GetText()) { - ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(w.GetTable().CmdBuff().GetText())) + if sel, err := ui.TrimLabelSelector(w.GetTable().CmdBuff().GetText()); err == nil { + ctx = context.WithValue(ctx, internal.KeyLabels, sel) + } } ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(w.App().Config.ActiveNamespace())) ctx = context.WithValue(ctx, internal.KeyWithMetrics, w.App().factory.Client().HasMetrics()) diff --git a/internal/view/xray.go b/internal/view/xray.go index c97c5733..048b11a4 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/labels" "k8s.io/apimachinery/pkg/util/sets" ) @@ -58,7 +59,7 @@ func NewXray(gvr *client.GVR) ResourceViewer { func (*Xray) SetCommand(*cmd.Interpreter) {} func (*Xray) SetFilter(string) {} -func (*Xray) SetLabelFilter(map[string]string) {} +func (*Xray) SetLabelSelector(labels.Selector) {} // Init initializes the view. func (x *Xray) Init(ctx context.Context) error { @@ -609,9 +610,11 @@ func (x *Xray) defaultContext() context.Context { ctx := context.WithValue(context.Background(), internal.KeyFactory, x.app.factory) ctx = context.WithValue(ctx, internal.KeyFields, "") if x.CmdBuff().Empty() { - ctx = context.WithValue(ctx, internal.KeyLabels, "") + ctx = context.WithValue(ctx, internal.KeyLabels, labels.Everything()) } else { - ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(x.CmdBuff().GetText())) + if sel, err := ui.TrimLabelSelector(x.CmdBuff().GetText()); err == nil { + ctx = context.WithValue(ctx, internal.KeyLabels, sel) + } } return ctx @@ -688,7 +691,9 @@ func (x *Xray) styleTitle() string { return title } if internal.IsLabelSelector(buff) { - buff = ui.TrimLabelSelector(buff) + if sel, err := ui.TrimLabelSelector(buff); err == nil { + buff = sel.String() + } } return title + ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), &styles) diff --git a/internal/vul/scanner.go b/internal/vul/scanner.go index e235d46b..7f2de16d 100644 --- a/internal/vul/scanner.go +++ b/internal/vul/scanner.go @@ -14,18 +14,18 @@ import ( "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" "github.com/anchore/grype/grype" - "github.com/anchore/grype/grype/db/legacy/distribution" - v5 "github.com/anchore/grype/grype/db/v5" - "github.com/anchore/grype/grype/db/v5/matcher" - "github.com/anchore/grype/grype/db/v5/matcher/dotnet" - "github.com/anchore/grype/grype/db/v5/matcher/golang" - "github.com/anchore/grype/grype/db/v5/matcher/java" - "github.com/anchore/grype/grype/db/v5/matcher/javascript" - "github.com/anchore/grype/grype/db/v5/matcher/python" - "github.com/anchore/grype/grype/db/v5/matcher/ruby" - "github.com/anchore/grype/grype/db/v5/matcher/stock" + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/matcher" + "github.com/anchore/grype/grype/matcher/dotnet" + "github.com/anchore/grype/grype/matcher/golang" + "github.com/anchore/grype/grype/matcher/java" + "github.com/anchore/grype/grype/matcher/javascript" + "github.com/anchore/grype/grype/matcher/python" + "github.com/anchore/grype/grype/matcher/ruby" + "github.com/anchore/grype/grype/matcher/stock" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vex" + "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/syft/syft" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/slogs" @@ -40,8 +40,8 @@ const ( ) type imageScanner struct { - store *v5.ProviderStore - dbStatus *distribution.Status + provider vulnerability.Provider + status *vulnerability.ProviderStatus opts *options.Grype scans Scans mx sync.RWMutex @@ -90,8 +90,9 @@ func (s *imageScanner) Init(name, version string) { s.opts.GenerateMissingCPEs = true var err error - s.store, s.dbStatus, err = grype.LoadVulnerabilityDB( - s.opts.DB.ToLegacyCuratorConfig(), + s.provider, s.status, err = grype.LoadVulnerabilityDB( + s.opts.ToClientConfig(), + s.opts.ToCuratorConfig(), s.opts.DB.AutoUpdate, ) if err != nil { @@ -99,7 +100,7 @@ func (s *imageScanner) Init(name, version string) { return } - if e := validateDBLoad(err, s.dbStatus); e != nil { + if e := validateDBLoad(err, s.status); e != nil { s.log.Error("VulDb validate failed", slogs.Error, e) return } @@ -112,9 +113,9 @@ func (s *imageScanner) Stop() { s.mx.RLock() defer s.mx.RUnlock() - if s.store != nil { - _ = s.store.Close() - s.store = nil + if s.provider != nil { + _ = s.provider.Close() + s.provider = nil } } @@ -180,11 +181,11 @@ func (s *imageScanner) scan(_ context.Context, img string, sc *Scan) error { } v := grype.VulnerabilityMatcher{ - Store: *s.store, - IgnoreRules: s.opts.Ignore, - NormalizeByCVE: s.opts.ByCVE, - FailSeverity: s.opts.FailOnSeverity(), - Matchers: getMatchers(s.opts), + VulnerabilityProvider: s.provider, + IgnoreRules: s.opts.Ignore, + NormalizeByCVE: s.opts.ByCVE, + FailSeverity: s.opts.FailOnSeverity(), + Matchers: getMatchers(s.opts), VexProcessor: vex.NewProcessor(vex.ProcessorOptions{ Documents: s.opts.VexDocuments, IgnoreRules: s.opts.Ignore, @@ -195,7 +196,7 @@ func (s *imageScanner) scan(_ context.Context, img string, sc *Scan) error { if err != nil { errs = errors.Join(errs, err) } - if err := sc.run(mm, s.store); err != nil { + if err := sc.run(mm, s.provider); err != nil { errs = errors.Join(errs, err) } @@ -218,7 +219,7 @@ func getProviderConfig(opts *options.Grype) pkg.ProviderConfig { } } -func getMatchers(opts *options.Grype) []matcher.Matcher { +func getMatchers(opts *options.Grype) []match.Matcher { return matcher.NewDefaultMatchers( matcher.Config{ Java: java.MatcherConfig{ @@ -230,23 +231,23 @@ func getMatchers(opts *options.Grype) []matcher.Matcher { Dotnet: dotnet.MatcherConfig(opts.Match.Dotnet), Javascript: javascript.MatcherConfig(opts.Match.Javascript), Golang: golang.MatcherConfig{ - UseCPEs: opts.Match.Golang.UseCPEs, - AlwaysUseCPEForStdlib: opts.Match.Golang.AlwaysUseCPEForStdlib, + UseCPEs: opts.Match.Golang.UseCPEs, + AlwaysUseCPEForStdlib: opts.Match.Golang.AlwaysUseCPEForStdlib, + AllowMainModulePseudoVersionComparison: opts.Match.Golang.AllowMainModulePseudoVersionComparison, }, Stock: stock.MatcherConfig(opts.Match.Stock), }, ) } - -func validateDBLoad(loadErr error, status *distribution.Status) error { +func validateDBLoad(loadErr error, status *vulnerability.ProviderStatus) error { if loadErr != nil { return fmt.Errorf("failed to load vulnerability db: %w", loadErr) } if status == nil { return fmt.Errorf("unable to determine the status of the vulnerability db") } - if status.Err != nil { - return fmt.Errorf("db could not be loaded: %w", status.Err) + if status.Error != nil { + return fmt.Errorf("db could not be loaded: %w", status.Error) } return nil diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 0227c9a4..3d1f57ad 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: k9s base: core22 -version: 'v0.50.3' +version: 'v0.50.4' 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.