diff --git a/.gitignore b/.gitignore index 953aa0a45f..80b1e5a991 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ .envrc cov.out execs -k9s +/k9s /k8s dist notes @@ -23,3 +23,4 @@ demos /code kind *.snap +/stresser diff --git a/.goreleaser.yml b/.goreleaser.yml index ae771143bb..3788ceed95 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,8 +1,10 @@ project_name: k9s + before: hooks: - go mod download - go generate ./... + release: prerelease: false diff --git a/Dockerfile b/Dockerfile index 81dfab66a6..5b5a39675d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN apk --no-cache add --update make libx11-dev git gcc libc-dev curl && make bu # Build the final Docker image FROM alpine:3.19.0 -ARG KUBECTL_VERSION="v1.27.3" +ARG KUBECTL_VERSION="v1.29.0" COPY --from=build /k9s/execs/k9s /bin/k9s RUN apk add --update ca-certificates \ diff --git a/Makefile b/Makefile index 7409077e08..06dfb4c1cd 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.29.0 +VERSION ?= v0.30.0 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} diff --git a/README.md b/README.md index bf160babc5..a94cb862ae 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,10 @@ for changes and offers subsequent commands to interact with your observed resour ## Note... -As you may know k9s is not pimped out by a big corporation with deep pockets. It is a complex OSS project that demands a lot of my time to maintain and support. K9s will always remain OSS and therefore free! That said if you feel, k9s makes your day to day Kubernetes journey a tad brighter, please consider sponsoring us or purchase a [K9sAlpha license](https://k9salpha.io). Your donations will go a long way in keeping our servers lights on and beers in our fridge! +K9s is not pimped out by a big corporation with deep pockets. +It is a complex OSS project that demands a lot of my time to maintain and support. +K9s will always remain OSS and therefore free! That said, if you feel k9s makes your day to day Kubernetes journey a tad brighter, saves you time and makes you more productive, please consider [sponsoring us!](https://github.com/sponsors/derailed) +Your donations will go a long way in keeping our servers lights on and beers in our fridge! **Thank you!** @@ -28,6 +31,35 @@ As you may know k9s is not pimped out by a big corporation with deep pockets. It --- +## Screenshots + +1. Pods + +2. Logs + +3. Deployments + + +--- + +## Demo Videos/Recordings + +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) +* [K9s v0.29.0](https://youtu.be/oiU3wmoAkBo) +* [K9s v0.21.3](https://youtu.be/wG8KCwDAhnw) +* [K9s v0.19.X](https://youtu.be/kj-WverKZ24) +* [K9s v0.18.0](https://www.youtube.com/watch?v=zMnD5e53yRw) +* [K9s v0.17.0](https://www.youtube.com/watch?v=7S33CNLAofk&feature=youtu.be) +* [K9s Pulses](https://asciinema.org/a/UbXKPal6IWpTaVAjBBFmizcGN) +* [K9s v0.15.1](https://youtu.be/7Fx4XQ2ftpM) +* [K9s v0.13.0](https://www.youtube.com/watch?v=qaeR2iK7U0o&t=15s) +* [K9s v0.9.0](https://www.youtube.com/watch?v=bxKfqumjW4I) +* [K9s v0.7.0 Features](https://youtu.be/83jYehwlql8) +* [K9s v0 Demo](https://youtu.be/k7zseUhaXeU) + +--- + ## Documentation Please refer to our [K9s documentation](https://k9scli.io) site for installation, usage, customization and tips. @@ -42,8 +74,7 @@ Wanna discuss K9s features with your fellow `K9sers` or simply show your support ## Installation K9s is available on Linux, macOS and Windows platforms. - -* Binaries for Linux, Windows and Mac are available as tarballs in the [release](https://github.com/derailed/k9s/releases) page. +Binaries for Linux, Windows and Mac are available as tarballs in the [release page](https://github.com/derailed/k9s/releases). * Via [Homebrew](https://brew.sh/) for macOS or Linux @@ -56,6 +87,7 @@ K9s is available on Linux, macOS and Windows platforms. ```shell sudo port install k9s ``` + * Via [snap](https://snapcraft.io/k9s) for Linux ```shell @@ -81,6 +113,7 @@ K9s is available on Linux, macOS and Windows platforms. ``` * Via [Winget](https://github.com/microsoft/winget-cli) for Windows + ```shell winget install k9s ``` @@ -132,7 +165,8 @@ K9s is available on Linux, macOS and Windows platforms. ## Building From Source - K9s is currently using go v1.14 or above. In order to build K9s from source you must: + K9s is currently using GO v1.21.X or above. + In order to build K9s from source you must: 1. Clone the repo 2. Build and run the executable @@ -164,7 +198,7 @@ K9s is available on Linux, macOS and Windows platforms. You can build your own Docker image of k9s from the [Dockerfile](Dockerfile) with the following: ```shell - docker build -t k9s-docker:0.1 . + docker build -t k9s-docker:v0.0.1 . ``` You can get the latest stable `kubectl` version and pass it to the `docker build` command with the `--build-arg` option. @@ -200,7 +234,7 @@ K9s is available on Linux, macOS and Windows platforms. export K9S_EDITOR=my_fav_editor ``` -* K9s prefers recent kubernetes versions ie 1.16+ +* K9s prefers recent kubernetes versions ie 1.28+ --- @@ -208,112 +242,111 @@ K9s is available on Linux, macOS and Windows platforms. | k9s | k8s client | | ------------------ | ---------- | -| >= v0.27.0 | 0.26.1 | -| v0.26.7 - v0.26.6 | 0.25.3 | -| v0.26.5 - v0.26.4 | 0.25.1 | -| v0.26.3 - v0.26.1 | 0.24.3 | -| v0.26.0 - v0.25.19 | 0.24.2 | -| v0.25.18 - v0.25.3 | 0.22.3 | -| v0.25.2 - v0.25.0 | 0.22.0 | -| <= v0.24 | 0.21.3 | +| >= v0.27.0 | 1.26.1 | +| v0.26.7 - v0.26.6 | 1.25.3 | +| v0.26.5 - v0.26.4 | 1.25.1 | +| v0.26.3 - v0.26.1 | 1.24.3 | +| v0.26.0 - v0.25.19 | 1.24.2 | +| v0.25.18 - v0.25.3 | 1.22.3 | +| v0.25.2 - v0.25.0 | 1.22.0 | +| <= v0.24 | 1.21.3 | --- ## The Command Line ```shell -# List all available CLI options -k9s help +# List current version +k9s version + # To get info about K9s runtime (logs, configs, etc..) k9s info + +# List all available CLI options +k9s help + # To run K9s in a given namespace k9s -n mycoolns + # Start K9s in an existing KubeConfig context k9s --context coolCtx + # Start K9s in readonly mode - with all cluster modification commands disabled k9s --readonly ``` -## Logs +## Logs And Debug Logs -Given the nature of the ui k9s does produce logs to a specific location. To view the logs and turn on debug mode, use the following commands: +Given the nature of the ui k9s does produce logs to a specific location. +To view the logs and turn on debug mode, use the following commands: ```shell +# Find out where the logs are stored k9s info -# Will produces something like this -# ____ __.________ -# | |/ _/ __ \______ -# | < \____ / ___/ -# | | \ / /\___ \ -# |____|__ \ /____//____ > -# \/ \/ -# -# Configuration: ~/Library/Preferences/k9s/config.yml -# Logs: /var/folders/8c/hh6rqbgs5nx_c_8k9_17ghfh0000gn/T/k9s-fernand.log -# Screen Dumps: /var/folders/8c/hh6rqbgs5nx_c_8k9_17ghfh0000gn/T/k9s-screens-fernand - -# To view k9s logs -tail -f /var/folders/8c/hh6rqbgs5nx_c_8k9_17ghfh0000gn/T/k9s-fernand.log - -# Start K9s in debug mode -k9s -l debug ``` -## Key Bindings - -K9s uses aliases to navigate most K8s resources. +```text + ____ __.________ +| |/ _/ __ \______ +| < \____ / ___/ +| | \ / /\___ \ +|____|__ \ /____//____ > + \/ \/ + +Version: vX.Y.Z +Config: /Users/fernand/.config/k9s/config.yaml +Logs: /Users/fernand/.local/state/k9s/k9s.log +Dumps dir: /Users/fernand/.local/state/k9s/screen-dumps +Benchmarks dir: /Users/fernand/.local/state/k9s/benchmarks +Skins dir: /Users/fernand/.local/share/k9s/skins +Contexts dir: /Users/fernand/.local/share/k9s/clusters +Custom views file: /Users/fernand/.local/share/k9s/views.yaml +Plugins file: /Users/fernand/.local/share/k9s/plugins.yaml +Hotkeys file: /Users/fernand/.local/share/k9s/hotkeys.yaml +Alias file: /Users/fernand/.local/share/k9s/aliases.yaml +``` -| Action | Command | Comment | -|----------------------------------------------------------------|-------------------------------|------------------------------------------------------------------------| -| Show active keyboard mnemonics and help | `?` | | -| Show all available resource alias | `ctrl-a` | | -| To bail out of K9s | `:q`, `ctrl-c` | | -| View a Kubernetes resource using singular/plural or short-name | `:`po⏎ | accepts singular, plural, short-name or alias ie pod or pods | -| View a Kubernetes resource in a given namespace | `:`alias namespace⏎ | | -| Filter out a resource view given a filter | `/`filter⏎ | Regex2 supported ie `fred|blee` to filter resources named fred or blee | -| Inverse regex filter | `/`! filter⏎ | Keep everything that *doesn't* match. | -| Filter resource view by labels | `/`-l label-selector⏎ | | -| Fuzzy find a resource given a filter | `/`-f filter⏎ | | -| Bails out of view/command/filter mode | `` | | -| Key mapping to describe, view, edit, view logs,... | `d`,`v`, `e`, `l`,... | | -| To view and switch to another Kubernetes context (Pod view) | `:`ctx⏎ | | -| To view and switch directly to another Kubernetes context (Last used view) | `:`ctx context-name⏎ | | -| To view and switch to another Kubernetes namespace | `:`ns⏎ | | -| To view all saved resources | `:`screendump or sd⏎ | | -| To delete a resource (TAB and ENTER to confirm) | `ctrl-d` | | -| To kill a resource (no confirmation dialog, equivalent to kubectl delete --now) | `ctrl-k` | | -| Launch pulses view | `:`pulses or pu⏎ | | -| Launch XRay view | `:`xray RESOURCE [NAMESPACE]⏎ | RESOURCE can be one of po, svc, dp, rs, sts, ds, NAMESPACE is optional | -| Launch Popeye view | `:`popeye or pop⏎ | See [popeye](#popeye) | +### View K9s logs ---- - -## Screenshots +```shell +tail -f /Users/fernand/.local/data/k9s/k9s.log +``` -1. Pods - -1. Logs - -1. Deployments - +### Start K9s in debug mode ---- +```shell +k9s -l debug +``` ---- +## Key Bindings -## Demo Videos/Recordings +K9s uses aliases to navigate most K8s resources. -* [k9s Kubernetes UI - A Terminal-Based Vim-Like Kubernetes Dashboard](https://youtu.be/boaW9odvRCc) -* [K9s v0.21.3](https://youtu.be/wG8KCwDAhnw) -* [K9s v0.19.X](https://youtu.be/kj-WverKZ24) -* [K9s v0.18.0](https://www.youtube.com/watch?v=zMnD5e53yRw) -* [K9s v0.17.0](https://www.youtube.com/watch?v=7S33CNLAofk&feature=youtu.be) -* [K9s Pulses](https://asciinema.org/a/UbXKPal6IWpTaVAjBBFmizcGN) -* [K9s v0.15.1](https://youtu.be/7Fx4XQ2ftpM) -* [K9s v0.13.0](https://www.youtube.com/watch?v=qaeR2iK7U0o&t=15s) -* [K9s v0.9.0](https://www.youtube.com/watch?v=bxKfqumjW4I) -* [K9s v0.7.0 Features](https://youtu.be/83jYehwlql8) -* [K9s v0 Demo](https://youtu.be/k7zseUhaXeU) +| Action | Command | Comment | +|---------------------------------------------------------------------------------|-------------------------------|------------------------------------------------------------------------| +| Show active keyboard mnemonics and help | `?` | | +| Show all available resource alias | `ctrl-a` | | +| To bail out of K9s | `:q`, `ctrl-c` | | +| View a Kubernetes resource using singular/plural or short-name | `:`pod⏎ | accepts singular, plural, short-name or alias ie pod or pods | +| View a Kubernetes resource in a given namespace | `:`pod ns-x⏎ | | +| View filtered pods (New v0.30.0!) | `:`pod /fred⏎ | View all pods filtered by fred | +| View labeled pods (New v0.30.0!) | `:`pod app=fred,env=dev⏎ | View all pods with labels matching app=fred and env=dev | +| View pods in a given context (New v0.30.0!) | `:`pod @ctx1⏎ | View all pods in context ctx1. Switches out your current k9s context! | +| Filter out a resource view given a filter | `/`filter⏎ | Regex2 supported ie `fred|blee` to filter resources named fred or blee | +| Inverse regex filter | `/`! filter⏎ | Keep everything that *doesn't* match. | +| Filter resource view by labels | `/`-l label-selector⏎ | | +| Fuzzy find a resource given a filter | `/`-f filter⏎ | | +| Bails out of view/command/filter mode | `` | | +| Key mapping to describe, view, edit, view logs,... | `d`,`v`, `e`, `l`,... | | +| To view and switch to another Kubernetes context (Pod view) | `:`ctx⏎ | | +| To view and switch directly to another Kubernetes context (Last used view) | `:`ctx context-name⏎ | | +| To view and switch to another Kubernetes namespace | `:`ns⏎ | | +| To view all saved resources | `:`screendump or sd⏎ | | +| To delete a resource (TAB and ENTER to confirm) | `ctrl-d` | | +| To kill a resource (no confirmation dialog, equivalent to kubectl delete --now) | `ctrl-k` | | +| Launch pulses view | `:`pulses or pu⏎ | | +| Launch XRay view | `:`xray RESOURCE [NAMESPACE]⏎ | RESOURCE can be one of po, svc, dp, rs, sts, ds, NAMESPACE is optional | +| Launch Popeye view | `:`popeye or pop⏎ | See [popeye](#popeye) | --- @@ -327,13 +360,13 @@ K9s uses aliases to navigate most K8s resources. > NOTE: This is still in flux and will change while in pre-release stage! - > NOTE! Thanks to [Mr Alexandru Placenta](https://github.com/placintaalexandru) the config files can now use either `.yml` or `.yaml` mimes. - ```yaml - # $XDG_CONFIG_HOME/k9s/config.yml + # $XDG_CONFIG_HOME/k9s/config.yaml k9s: # Enable periodic refresh of resource browser windows. Default false liveViewAutoRefresh: false + # The path to screen dump. Default: '%temp_dir%/k9s-screens-%username%' (k9s info) + screenDumpDir: /tmp/dumps # Represents ui poll intervals. Default 2secs refreshRate: 2 # Number of retries once the connection to the api-server is lost. Default 15. @@ -368,12 +401,6 @@ K9s uses aliases to navigate most K8s resources. textWrap: false # Toggles log line timestamp info. Default false showTime: false - # Indicates the current kube context. Defaults to current context - currentContext: minikube - # Indicates the current kube cluster. Defaults to current context cluster - currentCluster: minikube - # KeepMissingClusters will keep clusters in the config if they are missing from the current kubeconfig file. Default false - KeepMissingClusters: false # Provide shell pod customization when nodeShell feature gate is enabled! shellPod: # The shell pod image to use. @@ -386,41 +413,13 @@ K9s uses aliases to navigate most K8s resources. memory: 100Mi # Enable TTY tty: true - # Persists per cluster preferences for favorite namespaces and view. - clusters: - coolio: - namespace: - active: coolio - # With this set, the favorites list won't be updated as you switch namespaces - lockFavorites: false - favorites: - - cassandra - - default - view: - active: po - featureGates: - # Toggles NodeShell support. Allow K9s to shell into nodes if needed. Default false. - nodeShell: true - # The IP Address to use when launching a port-forward. - portForwardAddress: 1.2.3.4 - kind: - namespace: - active: all - favorites: - - all - - kube-system - - default - view: - active: dp - # The path to screen dump. Default: '%temp_dir%/k9s-screens-%username%' (k9s info) - screenDumpDir: /tmp ``` --- ## Popeye Configuration -K9s has integration with [Popeye](https://popeyecli.io/), which is a Kubernetes cluster sanitizer. Popeye itself uses a configuration called `spinach.yml`, but when integrating with K9s the cluster-specific file should be name `$XDG_CONFIG_HOME/k9s/_spinach.yml`. This allows you to have a different spinach config per cluster. +K9s has integration with [Popeye](https://popeyecli.io/), which is a Kubernetes cluster sanitizer. Popeye itself uses a configuration called `spinach.yml`, but when integrating with K9s the cluster-specific file should be name `$XDG_CONFIG_HOME/share/k9s/clusters/clusterX/contextY/spinach.yml`. This allows you to have a different spinach config per cluster. --- @@ -429,7 +428,7 @@ K9s has integration with [Popeye](https://popeyecli.io/), which is a Kubernetes By enabling the nodeShell feature gate on a given cluster, K9s allows you to shell into your cluster nodes. Once enabled, you will have a new `s` for `shell` menu option while in node view. K9s will launch a pod on the selected node using a special k9s_shell pod. Furthermore, you can refine your shell pod by using a custom docker image preloaded with the shell tools you love. By default k9s uses a BusyBox image, but you can configure it as follows: ```yaml -# $XDG_CONFIG_HOME/k9s/config.yml +# $XDG_CONFIG_HOME/k9s/config.yaml k9s: # You can also further tune the shell pod specification shellPod: @@ -438,41 +437,63 @@ k9s: limits: cpu: 100m memory: 100Mi - clusters: - # Configures node shell on cluster blee - blee: - featureGates: - # You must enable the nodeShell feature gate to enable shelling into nodes - nodeShell: true +``` + +Then in your cluster configuration file... + +```yaml +# $XDG_DATA_HOME/k9s/clusters/cluster-1/context-1 +k9s: + cluster: cluster-1 + readOnly: false + namespace: + active: default + lockFavorites: false + favorites: + - kube-system + - default + view: + active: po + featureGates: + nodeShell: true # => Enable this feature gate to make nodeShell available on this cluster + portForwardAddress: localhost ``` --- ## Command Aliases -In K9s, you can define your very own command aliases (shortnames) to access your resources. In your `$HOME/.config/k9s` define a file called `alias.yml`. A K9s alias defines pairs of alias:gvr. A gvr (Group/Version/Resource) represents a fully qualified Kubernetes resource identifier. Here is an example of an alias file: +In K9s, you can define your very own command aliases (shortnames) to access your resources. In your `$HOME/.config/k9s` define a file called `aliases.yaml`. +A K9s alias defines pairs of alias:gvr. A gvr (Group/Version/Resource) represents a fully qualified Kubernetes resource identifier. Here is an example of an alias file: ```yaml -# $XDG_CONFIG_HOME/k9s/alias.yml -alias: +# $XDG_DATA_HOME/k9s/aliases.yaml +aliases: pp: v1/pods crb: rbac.authorization.k8s.io/v1/clusterrolebindings + # As of v0.30.0 you can also refer to another command alias... + fred: pod fred app=blee # => view pods in namespace fred with labels matching app=blee ``` -Using this alias file, you can now type pp/crb to list pods or ClusterRoleBindings respectively. +Using this aliases file, you can now type `:pp` or `:crb` or `:fred` to activate their respective commands. --- ## HotKey Support -Entering the command mode and typing a resource name or alias, could be cumbersome for navigating thru often used resources. We're introducing hotkeys that allows a user to define their own hotkeys to activate their favorite resource views. In order to enable hotkeys please follow these steps: +Entering the command mode and typing a resource name or alias, could be cumbersome for navigating thru often used resources. +We're introducing hotkeys that allow users to define their own key combination to activate their favorite resource views. + +Additionally, you can define context specific hotkeys by add a context level configuration file in `$XDG_DATA_HOME/k9s/clusters/clusterX/contextY/hotkeys.yaml` -1. Create a file named `$XDG_CONFIG_HOME/k9s/hotkey.yml` -2. Add the following to your `hotkey.yml`. You can use resource name/short name to specify a command ie same as typing it while in command mode. +In order to surface hotkeys globally please follow these steps: + +1. Create a file named `$XDG_CONFIG_HOME/k9s/hotkeys.yaml` +2. Add the following to your `hotkeys.yaml`. You can use resource name/short name to specify a command ie same as typing it while in command mode. ```yaml - # $XDG_CONFIG_HOME/k9s/hotkey.yml - hotKey: + # $XDG_CONFIG_HOME/k9s/hotkeys.yaml + hotKeys: # Hitting Shift-0 navigates to your pod view shift-0: shortCut: Shift-0 @@ -490,7 +511,8 @@ Entering the command mode and typing a resource name or alias, could be cumberso command: xray deploy ``` - Not feeling so hot? Your custom hotkeys will be listed in the help view `?`. Also your hotkey file will be automatically reloaded so you can readily use your hotkeys as you define them. + Not feeling so hot? Your custom hotkeys will be listed in the help view `?`. + Also your hotkeys file will be automatically reloaded so you can readily use your hotkeys as you define them. You can choose any keyboard shortcuts that make sense to you, provided they are not part of the standard K9s shortcuts list. @@ -502,9 +524,9 @@ Entering the command mode and typing a resource name or alias, could be cumberso As of v0.25.0, you can leverage the `FastForwards` feature to tell K9s how to default port-forwards. In situations where you are dealing with multiple containers or containers exposing multiple ports, it can be cumbersome to specify the desired port-forward from the dialog as in most cases, you already know which container/port tuple you desire. For these use cases, you can now annotate your manifests with the following annotations: -- `k9scli.io/auto-port-forwards` +@ `k9scli.io/auto-port-forwards` activates one or more port-forwards directly bypassing the port-forward dialog all together. -- `k9scli.io/port-forwards` +@ `k9scli.io/port-forwards` pre-selects one or more port-forwards when launching the port-forward dialog. The annotation value takes on the shape `container-name::[local-port:]container-port` @@ -553,14 +575,14 @@ The annotation value must specify a container to forward to as well as a local p [SneakCast v0.17.0 on The Beach! - Yup! sound is sucking but what a setting!](https://youtu.be/7S33CNLAofk) -You can change which columns shows up for a given resource via custom views. To surface this feature, you will need to create a new configuration file, namely `$XDG_CONFIG_HOME/k9s/views.yml`. This file leverages GVR (Group/Version/Resource) to configure the associated table view columns. If no GVR is found for a view the default rendering will take over (ie what we have now). Going wide will add all the remaining columns that are available on the given resource after your custom columns. To boot, you can edit your views config file and tune your resources views live! +You can change which columns shows up for a given resource via custom views. To surface this feature, you will need to create a new configuration file, namely `$XDG_CONFIG_HOME/k9s/views.yaml`. This file leverages GVR (Group/Version/Resource) to configure the associated table view columns. If no GVR is found for a view the default rendering will take over (ie what we have now). Going wide will add all the remaining columns that are available on the given resource after your custom columns. To boot, you can edit your views config file and tune your resources views live! > NOTE: This is experimental and will most likely change as we iron this out! Here is a sample views configuration that customize a pods and services views. ```yaml -# $XDG_CONFIG_HOME/k9s/views.yml +# $XDG_CONFIG_HOME/k9s/views.yaml k9s: views: v1/pods: @@ -585,7 +607,9 @@ k9s: ## Plugins -K9s allows you to extend your command line and tooling by defining your very own cluster commands via plugins. K9s will look at `$XDG_CONFIG_HOME/k9s/plugin.yml` to locate all available plugins. A plugin is defined as follows: +K9s allows you to extend your command line and tooling by defining your very own cluster commands via plugins. K9s will look at `$XDG_CONFIG_HOME/k9s/plugins.yaml` to locate all available plugins. + +A plugin is defined as follows: * Shortcut option represents the key combination a user would type to activate the plugin * Confirm option (when enabled) lets you see the command that is going to be executed and gives you an option to confirm or prevent execution @@ -614,13 +638,13 @@ K9s does provide additional environment variables for you to customize your plug Curly braces can be used to embed an environment variable inside another string, or if the column name contains special characters. (e.g. `${NAME}-example` or `${COL-%CPU/L}`) -### Example +### Plugin Example -This defines a plugin for viewing logs on a selected pod using `ctrl-l` for shortcut. +This defines a plugin for viewing logs on a selected pod using `ctrl-l` as shortcut. ```yaml -# $XDG_CONFIG_HOME/k9s/plugin.yml -plugin: +# $XDG_DATA_HOME/k9s/plugins.yaml +plugins: # Defines a plugin to provide a `ctrl-l` shortcut to tail the logs while in pod view. fred: shortCut: Ctrl-L @@ -657,12 +681,14 @@ Initially, the benchmarks will run with the following defaults: * HTTP Verb: GET * Path: / -The PortForward view is backed by a new K9s config file namely: `$XDG_CONFIG_HOME/k9s/bench-.yml` (note: extension is `yml` and not `yaml`). Each cluster you connect to will have its own bench config file, containing the name of the K8s context for the cluster. Changes to this file should automatically update the PortForward view to indicate how you want to run your benchmarks. +The PortForward view is backed by a new K9s config file namely: `$XDG_DATA_HOME/k9s/clusters/clusterX/contextY/benchmarks.yaml`. Each cluster you connect to will have its own bench config file, containing the name of the K8s context for the cluster. Changes to this file should automatically update the PortForward view to indicate how you want to run your benchmarks. -Here is a sample benchmarks.yml configuration. Please keep in mind this file will likely change in subsequent releases! +Benchmarks result reports are stored in `$XDG_STATE_HOME/k9s/clusters/clusterX/contextY` + +Here is a sample benchmarks.yaml configuration. Please keep in mind this file will likely change in subsequent releases! ```yaml -# This file resides in $XDG_CONFIG_HOME/k9s/bench-mycontext.yml +# This file resides in $XDG_DATA_HOME/k9s/clusters/clusterX/contextY/benchmarks.yaml benchmarks: # Indicates the default concurrency and number of requests setting if a container or service rule does not match. defaults: @@ -810,36 +836,90 @@ Example: Dracula Skin ;) Dracula Skin -You can style K9s based on your own sense of look and style. Skins are YAML files, that enable a user to change the K9s presentation layer. K9s default skin is loaded from `$XDG_CONFIG_HOME/k9s/skin.yml`. If a skin file is detected then the skin will be loaded if not the current stock skin remains in effect. +You can style K9s based on your own sense of look and style. Skins are YAML files, that enable a user to change the K9s presentation layer. See this repo `skins` directory for examples. +You can skin k9s by default by specifying a UI.skin attribute. You can also change K9s skins based on the context you are connecting too. +In this case, you can specify a skin field on your cluster config aka `skin: dracula` (just the name of the skin file without the extension!) and copy this repo +`skins/dracula.yaml` to `$XDG_CONFIG_HOME/k9s/skins/` directory. -You can also change K9s skins based on the cluster you are connecting too. In this case, you can specify a skin field on your cluster config aka `skin: dracula` (just the name of the skin!) and copy this repo skins/dracula.yml to `$XDG_CONFIG_HOME/k9s/skins` directory. -Below is a sample skin file, more skins are available in the skins directory in this repo, just simply copy any of these in your k9s home dir as `skin.yml`. +In the case where your cluster spans several contexts, you can add a skin context configuration to your context configuration. +This is a collection of {context_name, skin} tuples (please see example below!) Colors can be defined by name or using a hex representation. Of recent, we've added a color named `default` to indicate a transparent background color to preserve your terminal background color settings if so desired. > NOTE: This is very much an experimental feature at this time, more will be added/modified if this feature has legs so thread accordingly! > NOTE: Please see [K9s Skins](https://k9scli.io/topics/skins/) for a list of available colors. +To skin a specific context and provided the file `in_the_navy.yaml` is present in your skins directory. + ```yaml -# Make cluster fred display in_the_navy skin when loaded... +# $XDG_DATA_HOME/k9s/clusters/clusterX/contextY/config.yaml k9s: - ... - clusters: - fred: - # Override the default skin and use this skin for this cluster. - # NOTE: Just the skin file name to extension! - skin: in_the_navy # -> Look for a skin file in ~/.config/k9s/skins/in_the_navy.yml - namespace: - ... - view: - active: pod - featureGates: - nodeShell: false - portForwardAddress: localhost + cluster: clusterX + skin: in_the_navy + readOnly: false + namespace: + active: default + lockFavorites: false + favorites: + - kube-system + - default + view: + active: po + featureGates: + nodeShell: false + portForwardAddress: localhost +``` + +You can also specify a default skin for all contexts in the root k9s config file as so: + +```yaml +k9s: + liveViewAutoRefresh: false + screenDumpDir: /tmp/dumps + refreshRate: 2 + maxConnRetry: 5 + readOnly: false + noExitOnCtrlC: false + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + noIcons: false + # By default all contexts wil use the dracula skin unless explicitly overridden in the context config file. + skin: dracula # => assumes the file skins/dracular.yaml is present in the $XDG_DATA_HOME/k9s/skins directory + skipLatestRevCheck: false + disablePodCounting: false + shellPod: + image: busybox + namespace: default + limits: + cpu: 100m + memory: 100Mi + imageScans: + enable: false + blackList: + namespaces: [] + labels: {} + logger: + tail: 100 + buffer: 5000 + sinceSeconds: -1 + fullScreenLogs: false + textWrap: false + showTime: false + thresholds: + cpu: + critical: 90 + warn: 70 + memory: + critical: 90 + warn: 70 ``` ```yaml -# in_the_navy.yml: Skin InTheNavy... +# $XDG_DATA_HOME/k9s/skins/in_the_navy.yaml +# Skin InTheNavy! k9s: # General K9s styles body: @@ -935,7 +1015,7 @@ that you want, please file an issue and if so inclined submit a PR! K9s will most likely blow up if... -1. You're running older versions of Kubernetes. K9s works best on Kubernetes latest. +1. You're running older versions of Kubernetes. K9s works best on later Kubernetes versions. 2. You don't have enough RBAC fu to manage your cluster. --- @@ -966,4 +1046,4 @@ We always enjoy hearing from folks who benefit from our work! --- -Imhotep  © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) +Imhotep  © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.30.0.md b/change_logs/release_v0.30.0.md new file mode 100644 index 0000000000..9f3be44dc5 --- /dev/null +++ b/change_logs/release_v0.30.0.md @@ -0,0 +1,313 @@ + + +# Release v0.30.0 + +## 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 ♭ + +Going back to the classics... + +* [Home For Christmas - Fats Domino](https://www.youtube.com/watch?v=ykAVdPz8o1Q) +* [Our Love - Al Jarreau](https://www.youtube.com/watch?v=9ztMe6GIwi8) +* [Body And Soul - Louis Armstrong](https://www.youtube.com/watch?v=2Gnz69TbqHQ) +* [On The Dunes - Donald Fagen](https://www.youtube.com/watch?v=QoVT3XcMVvk) +* [Ciao - Lucio Dalla](https://www.youtube.com/watch?v=qcqXcmKu_I4) +* [Basin Street Blues - Louis Prima](https://www.youtube.com/watch?v=IijXXXpUefM&list=RDIijXXXpUefM&start_radio=1) + +--- + +## A Word From Our Sponsors... + +To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!! + +* [Bojan](https://github.com/rbojan) + +> Sponsorship cancellations since the last release: **5!** 🥹 + +--- + +## 🎄 Feature Release! 🎄 + +🎅 Merry Christmas to all and Best wishes for the new year!!🧑‍🎄 + +--- + +### Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +### Breaking Bad! + +> ☢️ !!Prior to installing v0.30.0!! Please be sure to backup your k9s configs directories or move them somewhere safe!! + +> ☢️ Please watch the v0.30.0 Sneak peek series (links below) for detailed information. +> +> ☢️ Most K9s configuration files have either split or changed location or names on this drop!! + +> We recommend moving your current k9s config dirs to another location and start k9s from scratch and let it create and initialize the various configs +> to their new spec and location. You can then use your existing setup and patch with the new layout/spec. +> As of v0.30.0 all config files now use the `*.yaml` extension. We did our best to update all the docs to match the new version. +> If you find doc issues either file an issue or better yet submit a PR! + +Some of you might say: `You're on the roll their bud! Two breaking changes drops in a row!!` +Per the wise words of my beloved Grand mama! `One can't cook a decent meal without creating a mess!` +Not to mention we're still at v0.x.y so `Open season on breaking changes` is very much in full effect. + +Tho I have tested this drop quite a bit, there is a strong chance that I've broken some stuff. +The key here is to walk the fine line of improving k9s code base and features set with minimal impact to you. +As you know by now, I am committed to ease the pain and resolve issues quickly to get you all back up and running. + +From the scope changes in this release, I would caution that this drop will likely break you! +If so, worry not! We will fix the duds so we are `Happy as a Hippo` once again. + +There was a few issues with the way K9s persists it's configuration and various artifacts. So we rewrote it! +First and foremost all k9s related YAML resources, will now use the standard ".yaml" extension. +I think we've bloated the code checking for both extensions with no real actionable value! + +As it stands the main K9s configuration `config.yml` will now be static. These settings are now readonly! All the dynamic configurations that K9s manages now live in a new directory aka `clusters`. The clusters directory manages your k8s cluster/context configurations. So things like active view, namespace, favorites, etc... now live in this directory. K9s configurations are still managed using either xdg `XDG_CONFIG_HOME` or you can set `K9S_CONFIG_DIR` to specify a your preferred k9s configs location. Also all config files will now use the ".yaml" extension vs ".yml"!! + +So the main k9s configuration (static) now looks like this: + +```yaml +# $XDG_CONFIG_HOME/k9s/config.yaml +# File will be autogenerated will all the default fixins if not found in the config specification. +k9s: + liveViewAutoRefresh: false + refreshRate: 2 + maxConnRetry: 5 + readOnly: false + noExitOnCtrlC: false + ui: # NOTE! New level!! + enableMouse: false + headless: false + logoless: false + crumbsless: false + noIcons: false + skipLatestRevCheck: false + disablePodCounting: false + # ShellPod configuration applies to all your clusters + shellPod: + image: busybox:1.35.0 + namespace: default + limits: + cpu: 100m + memory: 100Mi + # ImageScan config changed from v0.29.0! + imageScans: + enable: false + # Now figures exclusions ie blacklist namespaces or specific workload labels + blackList: + # Exclude the following namespaces for image vulscans! + namespaces: + - kube-system + - fred + # Exclude the following labels from image vulscans! + labels: + k8s-app: + - kindnet + - bozo + env: + - dev + logger: + tail: 100 + buffer: 5000 + sinceSeconds: -1 + fullScreenLogs: false + textWrap: false + showTime: false + thresholds: + cpu: + critical: 90 + warn: 70 + memory: + critical: 90 + warn: 70 +``` + +Next context specific configurations that are managed by you and k9s live in the XDG data directory +i.e `$XDG_DATA_HOME/k9s/clusters` or `$K9S_CONFIG_DIR/clusters` if the env var is set. + +```text +$XDG_DATA_HOME/k9s +// Clusters tracks visited kubeconfig cluster/contexts +├── clusters +│ ├── fred +│ │ └── bozo +│ │ └── config.yaml +│ ├── bozorg +│ │ ├── kind-bozo-1 +│ │ │ └── config.yaml +│ │ ├── kind-bozo-2 +│ │ │ └── config.yaml +│ │ └── kind-bozo-3 +│ │ └── config.yaml +│ └── bumblebeetuna +│ └── blee +│ └── config.yaml +└── skins + ├── black_and_wtf.yaml + ├── dracula.yaml + ├── in_the_navy.yml + ├── ... +``` + +Now looking at a given context configuration i.e cluster-1/context-1/config.yaml + +```yaml +# $XDG_DATA_HOME/k9s/clusters/bumblebeetuna/blee/config.yaml +k9s: + cluster: bumblebeetuna + readOnly: false # [New!] you can now single out a given context and make it readonly. Woof! + skin: in_the_navy # [NEW!] you can also skin individual contexts. Woof Woof! + namespace: + active: all + lockFavorites: false + favorites: + - all + - kube-system + - default + view: + active: dp + featureGates: + nodeShell: false + portForwardAddress: localhost +``` + +Transient artifacts ie k9s logs, screen-dumps, benchmarks etc now live in the state config dir. + +```text +$XDG_STATE_HOME/k9s +├── k9s.log # K9s log files +└── screen-dumps + └── bumblebeetuna # Screen dumps location for context blee + └── blee + └── deployments-kube-system-1703018199222861000.csv +``` + +If you get stuck or if my instructions are just `clear as mud`... `k9s info` is always your friend!! + +I feel this is an improvement (tho I might be unanimous on this!) especially for folks dealing with multi-clusters or swapping out there kubeconfigs... + +> NOTE! Paint is still fresh on this deal. Proceed with caution and please help us flush this feature out! + +--- + +# Got Prompt? + +In this drop, we've also gave the k9s command prompt aka `:xxx` some love. +You have the ability to specify filter directly in the prompt. + +So for example, you can now run something like `:po /fred` to run pod view with a filter to just show pods containing `fred`. Likewise `:po k8s-app=fred,env=blee` to filter by labels. +And now for the`Krampus` special... you can see pods in a different context all together via `:pod @ctx-2`. +Finally you can combo and send the `whole enchilada` via `:po k8s-app=fred /blee ns-1 @ctx-x` +Did I mention with completion where applicable? Yes Please!! +Compliments of [Jayson Wang](https://github.com/wjiec). Be sure to thank him!! + +Put these frequent flyers command in an alias and now you can nav your clusters with `even more style`! + +--- + +# All Is Love? + +🎵 `On The twentieth day of Christmas my true love gave to me... Ten worklords a-leaping??...` 🎵 + +This is a feature reported by many of you and its (finally!) here. As of this drop, we intro the `workload` view aka `wk` which is similar to `kubetcl get all`. I was reluctant to intro it given the potential hazards on larger clusters but figured why not? YOLO. I think using it in combo with the prompt updates it could pack a serious punch to observe workload related artifacts. + +--- + +# The Black List... + +As it seems customary with all k9s new features, folks want to turn them off ;( +The `Vulscan` feature did not get out unscaped ;( +As it was rightfully so pointed out, you may want to opted out scans for images that you do not control. +Tho I think it might be a good idea to run wide open once in a while to see if your cluster has any holes?? +For this reason, we've opted to intro a blacklist section under the image scan configuration to exclude certain images from the scans. + +Here is a sample configuration: + +```yaml +k9s: + liveViewAutoRefresh: false + refreshRate: 2 + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + noIcons: false + imageScans: + enable: true + blackList: + # Skip scans on these namespaces + namespaces: + - ns-1 + - ns-2 + # Skip scans for pods matching these labels + labels: + - app: + - fred + - blee + - duh + - env: + - dev +``` + +This is a bit of a blur now, but I think that it! We hope you guys will dig this drop or at least the concepts as likely this is going to be `Open Season` on bugs ;( + +🎵 `On The second day of Christmas my true love gave to me... Eleven buggers bugging??...` 🎵 + +Lastly looks like the sponsorship stream is down to an alarming trickle so if you dig this project and find it useful be sure `to give til it hurts!` + +--- + +🎅 Best wishes to you and yours for good health and happiness this holiday season!! 🎉 + +AndJoy! +Fernand + +--- + +## Resolved Issues + +* [#2346](https://github.com/derailed/k9s/issues/2346) k9s should not write state to config.yaml +* [#2335](https://github.com/derailed/k9s/issues/2335) Restore 0.28 column order on pod view bug +* [#2331](https://github.com/derailed/k9s/issues/2331) Set a shortcut key to run Vuln Scanning on a resource. Don't scan every resource at every startup. +* [#2283](https://github.com/derailed/k9s/issues/2283) Adding auto complete in search bar + +--- + +## 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!! + +* [#2357](https://github.com/derailed/k9s/pull/2357) Added ln check for snap +* [#2350](https://github.com/derailed/k9s/pull/2350) Add symlink into snap +* [#2348](https://github.com/derailed/k9s/pull/2348) Fix(misc plugins): split up multiline commands, use less -K everywhere +* [#2343](https://github.com/derailed/k9s/pull/2343) Passing on the correct suggestion parameters +* [#2341](https://github.com/derailed/k9s/pull/2340) Adding value, yaml and describe views to helm-history +* [#2340](https://github.com/derailed/k9s/pull/2340) Add pkgx to installation section + +--- + + © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/cmd/info.go b/cmd/info.go index 1ac5a5ff5f..719c946176 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -5,7 +5,6 @@ package cmd import ( "fmt" - "os" "github.com/derailed/k9s/internal/color" @@ -19,21 +18,31 @@ import ( func infoCmd() *cobra.Command { return &cobra.Command{ Use: "info", - Short: "Print configuration info", - Long: "Print configuration information", - Run: func(cmd *cobra.Command, args []string) { - printInfo() - }, + Short: "List K9s configurations info", + RunE: printInfo, } } -func printInfo() { - const fmat = "%-25s %s\n" +func printInfo(cmd *cobra.Command, args []string) error { + if err := config.InitLocs(); err != nil { + return err + } + const fmat = "%-27s %s\n" printLogo(color.Cyan) - printTuple(fmat, "Configuration", config.K9sConfigFile, color.Cyan) - printTuple(fmat, "Logs", config.DefaultLogFile, color.Cyan) - printTuple(fmat, "Screen Dumps", getScreenDumpDirForInfo(), color.Cyan) + printTuple(fmat, "Version", version, color.Cyan) + printTuple(fmat, "Config", config.AppConfigFile, color.Cyan) + printTuple(fmat, "Custom Views", config.AppViewsFile, color.Cyan) + printTuple(fmat, "Plugins", config.AppPluginsFile, color.Cyan) + printTuple(fmat, "Hotkeys", config.AppHotKeysFile, color.Cyan) + printTuple(fmat, "Aliases", config.AppAliasesFile, color.Cyan) + printTuple(fmat, "Skins", config.AppSkinsDir, color.Cyan) + printTuple(fmat, "Context Configs", config.AppContextsDir, color.Cyan) + printTuple(fmat, "Logs", config.AppLogFile, color.Cyan) + printTuple(fmat, "Benchmarks", config.AppBenchmarksDir, color.Cyan) + printTuple(fmat, "ScreenDumps", getScreenDumpDirForInfo(), color.Cyan) + + return nil } func printLogo(c color.Paint) { @@ -45,23 +54,20 @@ func printLogo(c color.Paint) { // getScreenDumpDirForInfo get default screen dump config dir or from config.K9sConfigFile configuration. func getScreenDumpDirForInfo() string { - if config.K9sConfigFile == "" { - return config.K9sDefaultScreenDumpDir + if config.AppConfigFile == "" { + return config.AppDumpsDir } - f, err := os.ReadFile(config.K9sConfigFile) + f, err := os.ReadFile(config.AppConfigFile) if err != nil { log.Error().Err(err).Msgf("Reads k9s config file %v", err) - return config.K9sDefaultScreenDumpDir + return config.AppDumpsDir } var cfg config.Config if err := yaml.Unmarshal(f, &cfg); err != nil { log.Error().Err(err).Msgf("Unmarshal k9s config %v", err) - return config.K9sDefaultScreenDumpDir - } - if cfg.K9s == nil { - cfg.K9s = config.NewK9s() + return config.AppDumpsDir } return cfg.K9s.GetScreenDumpDir() diff --git a/cmd/info_test.go b/cmd/info_test.go index 6f2a240780..e181848502 100644 --- a/cmd/info_test.go +++ b/cmd/info_test.go @@ -16,32 +16,31 @@ func Test_getScreenDumpDirForInfo(t *testing.T) { expectedScreenDumpDir string }{ "withK9sConfigFile": { - k9sConfigFile: "testdata/k9s.yml", + k9sConfigFile: "testdata/k9s.yaml", expectedScreenDumpDir: "/tmp", }, "withEmptyK9sConfigFile": { k9sConfigFile: "", - expectedScreenDumpDir: config.K9sDefaultScreenDumpDir, + expectedScreenDumpDir: config.AppDumpsDir, }, "withInvalidK9sConfigFilePath": { k9sConfigFile: "invalid", - expectedScreenDumpDir: config.K9sDefaultScreenDumpDir, + expectedScreenDumpDir: config.AppDumpsDir, }, "withScreenDumpDirEmptyInK9sConfigFile": { - k9sConfigFile: "testdata/k9s1.yml", - expectedScreenDumpDir: config.K9sDefaultScreenDumpDir, + k9sConfigFile: "testdata/k9s1.yaml", + expectedScreenDumpDir: config.AppDumpsDir, }, } for k := range tests { u := tests[k] t.Run(k, func(t *testing.T) { - initK9sConfigFile := config.K9sConfigFile - - config.K9sConfigFile = u.k9sConfigFile + initK9sConfigFile := config.AppConfigFile + config.AppConfigFile = u.k9sConfigFile assert.Equal(t, u.expectedScreenDumpDir, getScreenDumpDirForInfo()) - config.K9sConfigFile = initK9sConfigFile + config.AppConfigFile = initK9sConfigFile }) } } diff --git a/cmd/root.go b/cmd/root.go index 882b96e270..7a3bfc17da 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,6 +8,8 @@ import ( "os" "runtime/debug" + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/color" "github.com/derailed/k9s/internal/config" @@ -20,12 +22,12 @@ import ( ) const ( - appName = "k9s" + appName = config.AppName shortAppDesc = "A graphical CLI for your Kubernetes cluster management." longAppDesc = "K9s is a CLI to view and manage your Kubernetes clusters." ) -var _ config.KubeSettings = (*client.Config)(nil) +var _ data.KubeSettings = (*client.Config)(nil) var ( version, commit, date = "dev", "dev", client.NA @@ -43,6 +45,10 @@ var ( ) func init() { + if err := config.InitLogLoc(); err != nil { + fmt.Printf("Fail to init k9s logs location %s\n", err) + } + rootCmd.AddCommand(versionCmd(), infoCmd()) initK9sFlags() initK8sFlags() @@ -51,18 +57,21 @@ func init() { // Execute root command. func Execute() { if err := rootCmd.Execute(); err != nil { - log.Panic().Err(err) + panic(err) } } func run(cmd *cobra.Command, args []string) error { - if err := config.EnsureDirPath(*k9sFlags.LogFile, config.DefaultDirMod); err != nil { + if err := config.InitLocs(); err != nil { return err } - mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY - file, err := os.OpenFile(*k9sFlags.LogFile, mod, config.DefaultFileMod) + file, err := os.OpenFile( + *k9sFlags.LogFile, + os.O_CREATE|os.O_APPEND|os.O_WRONLY, + data.DefaultFileMod, + ) if err != nil { - return err + return fmt.Errorf("Log file %q init failed: %w", *k9sFlags.LogFile, err) } defer func() { if file != nil { @@ -80,8 +89,8 @@ func run(cmd *cobra.Command, args []string) error { }() log.Logger = log.Output(zerolog.ConsoleWriter{Out: file}) - zerolog.SetGlobalLevel(parseLevel(*k9sFlags.LogLevel)) + app := view.NewApp(loadConfiguration()) if err := app.Init(version, *k9sFlags.RefreshRate); err != nil { return err @@ -99,38 +108,24 @@ func run(cmd *cobra.Command, args []string) error { func loadConfiguration() *config.Config { log.Info().Msg("🐶 K9s starting up...") - // Load K9s config file... k8sCfg := client.NewConfig(k8sFlags) k9sCfg := config.NewConfig(k8sCfg) - - if err := k9sCfg.Load(config.K9sConfigFile); err != nil { + if err := k9sCfg.Load(config.AppConfigFile); err != nil { log.Warn().Msg("Unable to locate K9s config. Generating new configuration...") + k9sCfg.K9s.Generate(k9sFlags) } - - if *k9sFlags.RefreshRate != config.DefaultRefreshRate { - k9sCfg.K9s.OverrideRefreshRate(*k9sFlags.RefreshRate) - } - - k9sCfg.K9s.OverrideHeadless(*k9sFlags.Headless) - k9sCfg.K9s.OverrideLogoless(*k9sFlags.Logoless) - k9sCfg.K9s.OverrideCrumbsless(*k9sFlags.Crumbsless) - k9sCfg.K9s.OverrideReadOnly(*k9sFlags.ReadOnly) - k9sCfg.K9s.OverrideWrite(*k9sFlags.Write) - k9sCfg.K9s.OverrideCommand(*k9sFlags.Command) - k9sCfg.K9s.OverrideScreenDumpDir(*k9sFlags.ScreenDumpDir) - if err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil { log.Error().Err(err).Msgf("refine failed") } conn, err := client.InitConnection(k8sCfg) k9sCfg.SetConnection(conn) if err != nil { - log.Error().Err(err).Msgf("failed to connect to cluster %q", k9sCfg.K9s.CurrentContext) + log.Error().Err(err).Msgf("failed to connect to context %q", k9sCfg.K9s.ActiveContextName()) return k9sCfg } // Try to access server version if that fail. Connectivity issue? if !k9sCfg.GetConnection().CheckConnectivity() { - log.Panic().Msgf("Cannot connect to cluster %s", k9sCfg.K9s.CurrentCluster) + log.Panic().Msgf("Cannot connect to context %s", k9sCfg.K9s.ActiveContextName()) } if !k9sCfg.GetConnection().ConnectionOK() { panic("No connectivity") @@ -177,7 +172,7 @@ func initK9sFlags() { rootCmd.Flags().StringVarP( k9sFlags.LogFile, "logFile", "", - config.DefaultLogFile, + config.AppLogFile, "Specify the log file", ) rootCmd.Flags().BoolVar( diff --git a/cmd/testdata/k9s.yml b/cmd/testdata/k9s.yaml similarity index 100% rename from cmd/testdata/k9s.yml rename to cmd/testdata/k9s.yaml diff --git a/cmd/testdata/k9s1.yml b/cmd/testdata/k9s1.yaml similarity index 100% rename from cmd/testdata/k9s1.yml rename to cmd/testdata/k9s1.yaml diff --git a/go.mod b/go.mod index 9573a36397..1c246e6949 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/anchore/grype v0.73.4 github.com/atotto/clipboard v0.1.4 github.com/cenkalti/backoff/v4 v4.2.1 - github.com/derailed/popeye v0.11.1 + github.com/derailed/popeye v0.11.2 github.com/derailed/tcell/v2 v2.3.1-rc.3 github.com/derailed/tview v0.8.2 github.com/fatih/color v1.16.0 @@ -27,15 +27,15 @@ require ( github.com/stretchr/testify v1.8.4 golang.org/x/text v0.14.0 gopkg.in/yaml.v2 v2.4.0 - helm.sh/helm/v3 v3.13.3 - k8s.io/api v0.28.4 - k8s.io/apiextensions-apiserver v0.28.4 - k8s.io/apimachinery v0.28.4 - k8s.io/cli-runtime v0.28.4 - k8s.io/client-go v0.28.4 + helm.sh/helm/v3 v3.13.2 + k8s.io/api v0.29.0 + k8s.io/apiextensions-apiserver v0.29.0 + k8s.io/apimachinery v0.29.0 + k8s.io/cli-runtime v0.29.0 + k8s.io/client-go v0.29.0 k8s.io/klog/v2 v2.110.1 - k8s.io/kubectl v0.28.4 - k8s.io/metrics v0.28.4 + k8s.io/kubectl v0.29.0 + k8s.io/metrics v0.29.0 sigs.k8s.io/yaml v1.4.0 ) @@ -109,7 +109,7 @@ require ( github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect - github.com/emicklei/go-restful/v3 v3.10.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect @@ -153,6 +153,7 @@ require ( github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b // indirect @@ -216,9 +217,10 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/nwaples/rardecode v1.1.0 // indirect github.com/nxadm/tail v1.4.8 // indirect - github.com/onsi/gomega v1.27.10 // indirect + github.com/onsi/gomega v1.29.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc5 // indirect github.com/opencontainers/runc v1.1.5 // indirect @@ -310,10 +312,10 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/gorm v1.25.5 // indirect - k8s.io/apiserver v0.28.4 // indirect - k8s.io/component-base v0.28.4 // indirect - k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect - k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect + k8s.io/apiserver v0.29.0 // indirect + k8s.io/component-base v0.29.0 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect modernc.org/libc v1.29.0 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.7.2 // indirect @@ -322,5 +324,5 @@ require ( sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/go.sum b/go.sum index 841d9d4792..ba245aaa7c 100644 --- a/go.sum +++ b/go.sum @@ -384,8 +384,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da h1:ZOjWpVsFZ06eIhnh4mkaceTiVoktdU67+M7KDHJ268M= github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk= -github.com/derailed/popeye v0.11.1 h1:bjt5mXkcXY696ipuJqwY1sa5s3i431L9BlkQc6EuaqE= -github.com/derailed/popeye v0.11.1/go.mod h1:NkvjHH1F94tE7Ui17PlYiagQcFt7yXUV2hIhPzSK+0w= +github.com/derailed/popeye v0.11.2 h1:8MKMjYBJdYNktTKeh98TeT127jZY6CFAsurrENoTZCY= +github.com/derailed/popeye v0.11.2/go.mod h1:HygqX7A8BwidorJjJUnWDZ5AvbxHIU7uRwXgOtn9GwY= github.com/derailed/tcell/v2 v2.3.1-rc.3 h1:9s1fmyRcSPRlwr/C9tcpJKCujbrtmPpST6dcMUD2piY= github.com/derailed/tcell/v2 v2.3.1-rc.3/go.mod h1:nf68BEL8fjmXQHJT3xZjoZFs2uXOzyJcNAQqGUEMrFY= github.com/derailed/tview v0.8.2 h1:8b+QwVECV1lZ6VV7Vf1tergpJxJ+ReA/JhIBYyUVSFI= @@ -423,8 +423,8 @@ github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= -github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -670,6 +670,8 @@ github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= @@ -918,6 +920,8 @@ github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1n github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -926,10 +930,10 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= -github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= @@ -1204,8 +1208,8 @@ go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93V go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= -go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -1834,8 +1838,8 @@ gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -helm.sh/helm/v3 v3.13.3 h1:0zPEdGqHcubehJHP9emCtzRmu8oYsJFRrlVF3TFj8xY= -helm.sh/helm/v3 v3.13.3/go.mod h1:3OKO33yI3p4YEXtTITN2+4oScsHeQe71KuzhlZ+aPfg= +helm.sh/helm/v3 v3.13.2 h1:IcO9NgmmpetJODLZhR3f3q+6zzyXVKlRizKFwbi7K8w= +helm.sh/helm/v3 v3.13.2/go.mod h1:GIHDwZggaTGbedevTlrQ6DB++LBN6yuQdeGj0HNaDx0= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1843,30 +1847,30 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= -k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= -k8s.io/apiextensions-apiserver v0.28.4 h1:AZpKY/7wQ8n+ZYDtNHbAJBb+N4AXXJvyZx6ww6yAJvU= -k8s.io/apiextensions-apiserver v0.28.4/go.mod h1:pgQIZ1U8eJSMQcENew/0ShUTlePcSGFq6dxSxf2mwPM= -k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= -k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= -k8s.io/apiserver v0.28.4 h1:BJXlaQbAU/RXYX2lRz+E1oPe3G3TKlozMMCZWu5GMgg= -k8s.io/apiserver v0.28.4/go.mod h1:Idq71oXugKZoVGUUL2wgBCTHbUR+FYTWa4rq9j4n23w= -k8s.io/cli-runtime v0.28.4 h1:IW3aqSNFXiGDllJF4KVYM90YX4cXPGxuCxCVqCD8X+Q= -k8s.io/cli-runtime v0.28.4/go.mod h1:MLGRB7LWTIYyYR3d/DOgtUC8ihsAPA3P8K8FDNIqJ0k= -k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= -k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= -k8s.io/component-base v0.28.4 h1:c/iQLWPdUgI90O+T9TeECg8o7N3YJTiuz2sKxILYcYo= -k8s.io/component-base v0.28.4/go.mod h1:m9hR0uvqXDybiGL2nf/3Lf0MerAfQXzkfWhUY58JUbU= +k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= +k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= +k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= +k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= +k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= +k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= +k8s.io/apiserver v0.29.0 h1:Y1xEMjJkP+BIi0GSEv1BBrf1jLU9UPfAnnGGbbDdp7o= +k8s.io/apiserver v0.29.0/go.mod h1:31n78PsRKPmfpee7/l9NYEv67u6hOL6AfcE761HapDM= +k8s.io/cli-runtime v0.29.0 h1:q2kC3cex4rOBLfPOnMSzV2BIrrQlx97gxHJs21KxKS4= +k8s.io/cli-runtime v0.29.0/go.mod h1:VKudXp3X7wR45L+nER85YUzOQIru28HQpXr0mTdeCrk= +k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= +k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= +k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= +k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= -k8s.io/kubectl v0.28.4 h1:gWpUXW/T7aFne+rchYeHkyB8eVDl5UZce8G4X//kjUQ= -k8s.io/kubectl v0.28.4/go.mod h1:CKOccVx3l+3MmDbkXtIUtibq93nN2hkDR99XDCn7c/c= -k8s.io/metrics v0.28.4 h1:u36fom9+6c8jX2sk8z58H0hFaIUfrPWbXIxN7GT2blk= -k8s.io/metrics v0.28.4/go.mod h1:bBqAJxH20c7wAsTQxDXOlVqxGMdce49d7WNr1WeaLac= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/kubectl v0.29.0 h1:Oqi48gXjikDhrBF67AYuZRTcJV4lg2l42GmvsP7FmYI= +k8s.io/kubectl v0.29.0/go.mod h1:0jMjGWIcMIQzmUaMgAzhSELv5WtHo2a8pq67DtviAJs= +k8s.io/metrics v0.29.0 h1:a6dWcNM+EEowMzMZ8trka6wZtSRIfEA/9oLjuhBksGc= +k8s.io/metrics v0.29.0/go.mod h1:UCuTT4dC/x/x6ODSk87IWIZQnuAfcwxOjb1gjWJdjMA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs= modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= @@ -1886,8 +1890,8 @@ sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKU sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY= sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U= sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/client/client.go b/internal/client/client.go index fc5c9f2877..577c3a3fb5 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -14,7 +14,6 @@ import ( "github.com/rs/zerolog/log" authorizationv1 "k8s.io/api/authorization/v1" - v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/cache" "k8s.io/apimachinery/pkg/version" @@ -35,8 +34,8 @@ const ( var supportedMetricsAPIVersions = []string{"v1beta1"} -// Namespaces tracks a collection of namespace names. -type Namespaces map[string]struct{} +// NamespaceNames tracks a collection of namespace names. +type NamespaceNames map[string]struct{} // APIClient represents a Kubernetes api client. type APIClient struct { @@ -86,7 +85,7 @@ func (a *APIClient) ConnectionOK() bool { func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview { if ns == ClusterScope { - ns = AllNamespaces + ns = BlankNamespace } spec := NewGVR(gvr) res := spec.GVR() @@ -107,9 +106,9 @@ func makeCacheKey(ns, gvr string, vv []string) string { return ns + ":" + gvr + "::" + strings.Join(vv, ",") } -// ActiveCluster returns the current cluster name. -func (a *APIClient) ActiveCluster() string { - c, err := a.config.CurrentClusterName() +// ActiveContext returns the current context name. +func (a *APIClient) ActiveContext() string { + c, err := a.config.CurrentContextName() if err != nil { log.Error().Msgf("Unable to located active cluster") return "" @@ -119,9 +118,10 @@ func (a *APIClient) ActiveCluster() string { // IsActiveNamespace returns true if namespaces matches. func (a *APIClient) IsActiveNamespace(ns string) bool { - if a.ActiveNamespace() == AllNamespaces { + if a.ActiveNamespace() == BlankNamespace { return true } + return a.ActiveNamespace() == ns } @@ -131,7 +131,7 @@ func (a *APIClient) ActiveNamespace() string { return ns } - return AllNamespaces + return BlankNamespace } func (a *APIClient) clearCache() { @@ -149,7 +149,7 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) return false, errors.New("ACCESS -- No API server connection") } if IsClusterWide(ns) { - ns = AllNamespaces + ns = BlankNamespace } key := makeCacheKey(ns, gvr, verbs) if v, ok := a.cache.Get(key); ok { @@ -212,14 +212,27 @@ func (a *APIClient) ServerVersion() (*version.Info, error) { return info, nil } -// ValidNamespaces returns all available namespaces. -func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) { +func (a *APIClient) IsValidNamespace(ns string) bool { + if IsAllNamespace(ns) { + return true + } + nn, err := a.ValidNamespaceNames() + if err != nil { + return false + } + _, ok := nn[ns] + + return ok +} + +// ValidNamespaceNames returns all available namespaces. +func (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) { if a == nil { return nil, fmt.Errorf("validNamespaces: no available client found") } if nn, ok := a.cache.Get("validNamespaces"); ok { - if nss, ok := nn.([]v1.Namespace); ok { + if nss, ok := nn.(NamespaceNames); ok { return nss, nil } } @@ -233,9 +246,13 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) { if err != nil { return nil, err } - a.cache.Add("validNamespaces", nn.Items, cacheExpiry) + nns := make(NamespaceNames, len(nn.Items)) + for _, n := range nn.Items { + nns[n.Name] = struct{}{} + } + a.cache.Add("validNamespaces", nns, cacheExpiry) - return nn.Items, nil + return nns, nil } // CheckConnectivity return true if api server is cool or false otherwise. diff --git a/internal/client/config.go b/internal/client/config.go index 6603850b4a..e7d47885e8 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -10,15 +10,14 @@ import ( "sync" "time" - v1 "k8s.io/api/core/v1" "k8s.io/cli-runtime/pkg/genericclioptions" restclient "k8s.io/client-go/rest" clientcmd "k8s.io/client-go/tools/clientcmd" - clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/tools/clientcmd/api" ) const ( - defaultCallTimeoutDuration time.Duration = 10 * time.Second + defaultCallTimeoutDuration time.Duration = 15 * time.Second // UsePersistentConfig caches client config to avoid reloads. UsePersistentConfig = true @@ -60,7 +59,7 @@ func (c *Config) Flags() *genericclioptions.ConfigFlags { return c.flags } -func (c *Config) RawConfig() (clientcmdapi.Config, error) { +func (c *Config) RawConfig() (api.Config, error) { return c.clientConfig().RawConfig() } @@ -72,11 +71,14 @@ func (c *Config) reset() {} // SwitchContext changes the kubeconfig context to a new cluster. func (c *Config) SwitchContext(name string) error { - if _, err := c.GetContext(name); err != nil { + ct, err := c.GetContext(name) + if err != nil { return fmt.Errorf("context %q does not exist", name) } + // !!BOZO!! Do you need to reset the flags? flags := genericclioptions.NewConfigFlags(UsePersistentConfig) - flags.Context = &name + flags.Context, flags.ClusterName = &name, &ct.Cluster + flags.Namespace = c.flags.Namespace flags.Timeout = c.flags.Timeout flags.KubeConfig = c.flags.KubeConfig c.flags = flags @@ -84,6 +86,22 @@ func (c *Config) SwitchContext(name string) error { return nil } +// CurrentClusterName returns the currently active cluster name. +func (c *Config) CurrentClusterName() (string, error) { + if isSet(c.flags.ClusterName) { + return *c.flags.ClusterName, nil + } + cfg, err := c.RawConfig() + if err != nil { + return "", err + } + + ct := cfg.Contexts[cfg.CurrentContext] + + return ct.Cluster, nil + +} + // CurrentContextName returns the currently active config context. func (c *Config) CurrentContextName() (string, error) { if isSet(c.flags.Context) { @@ -110,8 +128,17 @@ func (c *Config) CurrentContextNamespace() (string, error) { return context.Namespace, nil } +// CurrentContext returns the current context configuration. +func (c *Config) CurrentContext() (*api.Context, error) { + n, err := c.CurrentContextName() + if err != nil { + return nil, err + } + return c.GetContext(n) +} + // GetContext fetch a given context or error if it does not exists. -func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) { +func (c *Config) GetContext(n string) (*api.Context, error) { cfg, err := c.RawConfig() if err != nil { return nil, err @@ -124,7 +151,7 @@ func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) { } // Contexts fetch all available contexts. -func (c *Config) Contexts() (map[string]*clientcmdapi.Context, error) { +func (c *Config) Contexts() (map[string]*api.Context, error) { cfg, err := c.RawConfig() if err != nil { return nil, err @@ -180,63 +207,14 @@ func (c *Config) RenameContext(old string, new string) error { } // ContextNames fetch all available contexts. -func (c *Config) ContextNames() ([]string, error) { +func (c *Config) ContextNames() (map[string]struct{}, error) { cfg, err := c.RawConfig() if err != nil { return nil, err } - - cc := make([]string, 0, len(cfg.Contexts)) + cc := make(map[string]struct{}, len(cfg.Contexts)) for n := range cfg.Contexts { - cc = append(cc, n) - } - return cc, nil -} - -// ClusterNameFromContext returns the cluster associated with the given context. -func (c *Config) ClusterNameFromContext(context string) (string, error) { - cfg, err := c.RawConfig() - if err != nil { - return "", err - } - - if ctx, ok := cfg.Contexts[context]; ok { - return ctx.Cluster, nil - } - return "", fmt.Errorf("unable to locate cluster from context %s", context) -} - -// CurrentClusterName returns the active cluster name. -func (c *Config) CurrentClusterName() (string, error) { - if isSet(c.flags.ClusterName) { - return *c.flags.ClusterName, nil - } - cfg, err := c.RawConfig() - if err != nil { - return "", err - } - context, err := c.CurrentContextName() - if err != nil { - context = cfg.CurrentContext - } - - if ctx, ok := cfg.Contexts[context]; ok { - return ctx.Cluster, nil - } - - return "", errors.New("unable to locate current cluster") -} - -// ClusterNames fetch all kubeconfig defined clusters. -func (c *Config) ClusterNames() (map[string]struct{}, error) { - cfg, err := c.RawConfig() - if err != nil { - return nil, err - } - - cc := make(map[string]struct{}, len(cfg.Clusters)) - for name := range cfg.Clusters { - cc[name] = struct{}{} + cc[n] = struct{}{} } return cc, nil @@ -297,16 +275,17 @@ func (c *Config) CurrentUserName() (string, error) { // CurrentNamespaceName retrieves the active namespace. func (c *Config) CurrentNamespaceName() (string, error) { - ns, _, err := c.clientConfig().Namespace() - - if ns == "default" { - ns, err = c.CurrentContextNamespace() - if ns == "" && err == nil { - return "", errors.New("No namespace specified in context") - } + ns, overridden, err := c.clientConfig().Namespace() + if err != nil { + return BlankNamespace, err + } + // Checks if ns is passed is in args. + if overridden { + return ns, nil } - return ns, err + // Return ns set in context if any?? + return c.CurrentContextNamespace() } // ConfigAccess return the current kubeconfig api server access configuration. @@ -320,16 +299,6 @@ func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) { // ---------------------------------------------------------------------------- // Helpers... -// NamespaceNames fetch all available namespaces on current cluster. -func NamespaceNames(nns []v1.Namespace) []string { - nn := make([]string, 0, len(nns)) - for _, ns := range nns { - nn = append(nn, ns.Name) - } - - return nn -} - func isSet(s *string) bool { return s != nil && len(*s) != 0 } diff --git a/internal/client/config_test.go b/internal/client/config_test.go index 37a1029c03..dda1e39ff3 100644 --- a/internal/client/config_test.go +++ b/internal/client/config_test.go @@ -5,13 +5,12 @@ package client_test import ( "errors" + "os" "testing" "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -55,15 +54,15 @@ func TestConfigCurrentCluster(t *testing.T) { name, kubeConfig := "blee", "./testdata/config" uu := map[string]struct { flags *genericclioptions.ConfigFlags - cluster string + context string }{ "default": { flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, - cluster: "fred", + context: "fred", }, "custom": { - flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, ClusterName: &name}, - cluster: "blee", + flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Context: &name}, + context: "blee", }, } @@ -71,9 +70,9 @@ func TestConfigCurrentCluster(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { cfg := client.NewConfig(u.flags) - ctx, err := cfg.CurrentClusterName() + ct, err := cfg.CurrentContextName() assert.Nil(t, err) - assert.Equal(t, u.cluster, ctx) + assert.Equal(t, u.context, ct) }) } } @@ -173,8 +172,8 @@ func TestConfigGetContext(t *testing.T) { func TestConfigSwitchContext(t *testing.T) { cluster, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, + KubeConfig: &kubeConfig, + Context: &cluster, } cfg := client.NewConfig(&flags) @@ -185,24 +184,11 @@ func TestConfigSwitchContext(t *testing.T) { assert.Equal(t, "blee", ctx) } -func TestConfigClusterNameFromContext(t *testing.T) { - cluster, kubeConfig := "duh", "./testdata/config" - flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, - } - - cfg := client.NewConfig(&flags) - cl, err := cfg.ClusterNameFromContext("blee") - assert.Nil(t, err) - assert.Equal(t, "blee", cl) -} - func TestConfigAccess(t *testing.T) { - cluster, kubeConfig := "duh", "./testdata/config" + context, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, + KubeConfig: &kubeConfig, + Context: &context, } cfg := client.NewConfig(&flags) @@ -211,24 +197,11 @@ func TestConfigAccess(t *testing.T) { assert.True(t, len(acc.GetDefaultFilename()) > 0) } -func TestConfigContexts(t *testing.T) { - cluster, kubeConfig := "duh", "./testdata/config" - flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, - } - - cfg := client.NewConfig(&flags) - cc, err := cfg.Contexts() - assert.Nil(t, err) - assert.Equal(t, 3, len(cc)) -} - func TestConfigContextNames(t *testing.T) { cluster, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, + KubeConfig: &kubeConfig, + Context: &cluster, } cfg := client.NewConfig(&flags) @@ -237,33 +210,37 @@ func TestConfigContextNames(t *testing.T) { assert.Equal(t, 3, len(cc)) } -func TestConfigClusterNames(t *testing.T) { - cluster, kubeConfig := "duh", "./testdata/config" +func TestConfigContexts(t *testing.T) { + context, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, + KubeConfig: &kubeConfig, + Context: &context, } cfg := client.NewConfig(&flags) - cc, err := cfg.ClusterNames() + cc, err := cfg.Contexts() assert.Nil(t, err) assert.Equal(t, 3, len(cc)) } func TestConfigDelContext(t *testing.T) { - cluster, kubeConfig := "duh", "./testdata/config.1" + assert.NoError(t, cp("./testdata/config.2", "./testdata/config.1")) + + context, kubeConfig := "duh", "./testdata/config.1" flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, + KubeConfig: &kubeConfig, + Context: &context, } cfg := client.NewConfig(&flags) err := cfg.DelContext("fred") - assert.Nil(t, err) + assert.NoError(t, err) + cc, err := cfg.ContextNames() - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, 1, len(cc)) - assert.Equal(t, "blee", cc[0]) + _, ok := cc["blee"] + assert.True(t, ok) } func TestConfigRestConfig(t *testing.T) { @@ -289,13 +266,13 @@ func TestConfigBadConfig(t *testing.T) { assert.NotNil(t, err) } -func TestNamespaceNames(t *testing.T) { - nn := []v1.Namespace{ - {ObjectMeta: metav1.ObjectMeta{Name: "ns1"}}, - {ObjectMeta: metav1.ObjectMeta{Name: "ns2"}}, +// Helpers... + +func cp(src string, dst string) error { + data, err := os.ReadFile(src) + if err != nil { + return err } - nns := client.NamespaceNames(nn) - assert.Equal(t, 2, len(nns)) - assert.Equal(t, []string{"ns1", "ns2"}, nns) + return os.WriteFile(dst, data, 0600) } diff --git a/internal/client/gvr.go b/internal/client/gvr.go index 715e274dd7..fabb81f721 100644 --- a/internal/client/gvr.go +++ b/internal/client/gvr.go @@ -14,6 +14,8 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) +var NoGVR = GVR{} + // GVR represents a kubernetes resource schema as a string. // Format is group/version/resources:subresource. type GVR struct { diff --git a/internal/client/gvr_test.go b/internal/client/gvr_test.go index 714f1f454f..744ef26773 100644 --- a/internal/client/gvr_test.go +++ b/internal/client/gvr_test.go @@ -49,7 +49,7 @@ func TestGVRCan(t *testing.T) { } } -func TestAsGVR(t *testing.T) { +func TestGVR(t *testing.T) { uu := map[string]struct { gvr string e schema.GroupVersionResource diff --git a/internal/client/helpers.go b/internal/client/helpers.go index 68006ed132..204da6a547 100644 --- a/internal/client/helpers.go +++ b/internal/client/helpers.go @@ -17,13 +17,13 @@ var toFileName = regexp.MustCompile(`[^(\w/\.)]`) // IsClusterWide returns true if ns designates cluster scope, false otherwise. func IsClusterWide(ns string) bool { - return ns == NamespaceAll || ns == AllNamespaces || ns == ClusterScope + return ns == NamespaceAll || ns == BlankNamespace || ns == ClusterScope } // CleanseNamespace ensures all ns maps to blank. func CleanseNamespace(ns string) string { if IsAllNamespace(ns) { - return AllNamespaces + return BlankNamespace } return ns @@ -36,7 +36,7 @@ func IsAllNamespace(ns string) bool { // IsAllNamespaces returns true if all namespaces, false otherwise. func IsAllNamespaces(ns string) bool { - return ns == NamespaceAll || ns == AllNamespaces + return ns == NamespaceAll || ns == BlankNamespace } // IsNamespaced returns true if a specific ns is given. diff --git a/internal/client/metrics.go b/internal/client/metrics.go index 8ba7fd3568..fe2e90baf1 100644 --- a/internal/client/metrics.go +++ b/internal/client/metrics.go @@ -20,6 +20,8 @@ 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. @@ -149,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, "metrics.k8s.io/v1beta1/nodes", msg); err != nil { + if err := m.checkAccess(ClusterScope, nodeMXGVR, msg); err != nil { return mx, err } @@ -180,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, "metrics.k8s.io/v1beta1/nodes", msg); err != nil { + if err := m.checkAccess(ClusterScope, nodeMXGVR, msg); err != nil { return mx, err } @@ -218,9 +220,9 @@ func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1be const msg = "user is not authorized to list pods metrics" if ns == NamespaceAll { - ns = AllNamespaces + ns = BlankNamespace } - if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil { + if err := m.checkAccess(ns, podMXGVR, msg); err != nil { return mx, err } @@ -269,9 +271,9 @@ func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1be ns, _ := Namespaced(fqn) if ns == NamespaceAll { - ns = AllNamespaces + ns = BlankNamespace } - if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil { + if err := m.checkAccess(ns, podMXGVR, msg); err != nil { return mx, err } diff --git a/internal/client/testdata/config.2 b/internal/client/testdata/config.2 new file mode 100644 index 0000000000..efcf664750 --- /dev/null +++ b/internal/client/testdata/config.2 @@ -0,0 +1,23 @@ +apiVersion: v1 +clusters: +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:3001 + name: blee +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:3002 + name: fred +contexts: +- context: + cluster: blee + user: blee + name: blee +- context: + cluster: fred + user: fred + name: fred +current-context: blee +kind: Config +preferences: {} +users: null diff --git a/internal/client/types.go b/internal/client/types.go index fd814572da..24d66aad69 100644 --- a/internal/client/types.go +++ b/internal/client/types.go @@ -4,7 +4,6 @@ package client import ( - v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/discovery/cached/disk" "k8s.io/client-go/dynamic" @@ -21,8 +20,8 @@ const ( // NamespaceAll designates the fictional all namespace. NamespaceAll = "all" - // AllNamespaces designates all namespaces. - AllNamespaces = "" + // BlankNamespace designates no namespace. + BlankNamespace = "" // DefaultNamespace designates the default namespace. DefaultNamespace = "default" @@ -118,8 +117,11 @@ type Connection interface { // HasMetrics checks if metrics server is available. HasMetrics() bool - // ValidNamespaces returns all available namespaces. - ValidNamespaces() ([]v1.Namespace, error) + // ValidNamespaces returns all available namespace names. + ValidNamespaceNames() (NamespaceNames, error) + + // IsValidNamespace checks if given namespace is known. + IsValidNamespace(string) bool // ServerVersion returns current server version. ServerVersion() (*version.Info, error) @@ -127,8 +129,8 @@ type Connection interface { // CheckConnectivity checks if api server connection is happy or not. CheckConnectivity() bool - // ActiveCluster returns the current cluster name. - ActiveCluster() string + // ActiveContext returns the current context name. + ActiveContext() string // ActiveNamespace returns the current namespace. ActiveNamespace() string diff --git a/internal/config/alias.go b/internal/config/alias.go index f8900a018a..6ce8365a3c 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -5,16 +5,13 @@ package config import ( "os" - "path/filepath" "sync" + "github.com/derailed/k9s/internal/config/data" "github.com/rs/zerolog/log" "gopkg.in/yaml.v2" ) -// K9sAlias manages K9s aliases. -var K9sAlias = YamlExtension(filepath.Join(K9sHome(), "alias.yml")) - // Alias tracks shortname to GVR mappings. type Alias map[string]string @@ -23,7 +20,7 @@ type ShortNames map[string][]string // Aliases represents a collection of aliases. type Aliases struct { - Alias Alias `yaml:"alias"` + Alias Alias `yaml:"aliases"` mx sync.RWMutex } @@ -101,13 +98,28 @@ func (a *Aliases) Define(gvr string, aliases ...string) { } // Load K9s aliases. -func (a *Aliases) Load() error { +func (a *Aliases) Load(path string) error { a.loadDefaultAliases() - return a.LoadFileAliases(K9sAlias) + + f, err := EnsureAliasesCfgFile() + if err != nil { + log.Error().Err(err).Msgf("Unable to gen config aliases") + } + + // load global alias file + if err := a.LoadFile(f); err != nil { + return err + } + + // load context specific aliases if any + return a.LoadFile(path) } -// LoadFileAliases loads alias from a given file. -func (a *Aliases) LoadFileAliases(path string) error { +// LoadFile loads alias from a given file. +func (a *Aliases) LoadFile(path string) error { + if path == "" { + return nil + } f, err := os.ReadFile(path) if err == nil { var aa Aliases @@ -136,15 +148,6 @@ func (a *Aliases) loadDefaultAliases() { a.mx.Lock() defer a.mx.Unlock() - a.Alias["dp"] = "apps/v1/deployments" - a.Alias["sec"] = "v1/secrets" - a.Alias["jo"] = "batch/v1/jobs" - a.Alias["cr"] = "rbac.authorization.k8s.io/v1/clusterroles" - a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings" - a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles" - a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings" - a.Alias["np"] = "networking.k8s.io/v1/networkpolicies" - a.declare("help", "h", "?") a.declare("quit", "q", "q!", "qa", "Q") a.declare("aliases", "alias", "a") @@ -155,21 +158,22 @@ func (a *Aliases) loadDefaultAliases() { a.declare("users", "user", "usr") a.declare("groups", "group", "grp") a.declare("portforwards", "portforward", "pf") - a.declare("benchmarks", "bench", "benchmark", "be") + 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") } // Save alias to disk. func (a *Aliases) Save() error { log.Debug().Msg("[Config] Saving Aliases...") - return a.SaveAliases(K9sAlias) + return a.SaveAliases(AppAliasesFile) } // SaveAliases saves aliases to a given file. func (a *Aliases) SaveAliases(path string) error { - if err := EnsureDirPath(path, DefaultDirMod); err != nil { + if err := data.EnsureDirPath(path, data.DefaultDirMod); err != nil { return err } cfg, err := yaml.Marshal(a) diff --git a/internal/config/alias_test.go b/internal/config/alias_test.go index 3d40bc41a2..f65ddb9993 100644 --- a/internal/config/alias_test.go +++ b/internal/config/alias_test.go @@ -75,7 +75,7 @@ func TestAliasDefine(t *testing.T) { func TestAliasesLoad(t *testing.T) { a := config.NewAliases() - assert.Nil(t, a.LoadFileAliases("testdata/alias.yml")) + assert.Nil(t, a.LoadFile("testdata/alias.yaml")) assert.Equal(t, 2, len(a.Alias)) } @@ -84,7 +84,7 @@ func TestAliasesSave(t *testing.T) { a.Alias["test"] = "fred" a.Alias["blee"] = "duh" - assert.Nil(t, a.SaveAliases("/tmp/a.yml")) - assert.Nil(t, a.LoadFileAliases("/tmp/a.yml")) + assert.Nil(t, a.SaveAliases("/tmp/a.yaml")) + assert.Nil(t, a.LoadFile("/tmp/a.yaml")) assert.Equal(t, 2, len(a.Alias)) } diff --git a/internal/config/bench.go b/internal/config/benchmark.go similarity index 100% rename from internal/config/bench.go rename to internal/config/benchmark.go index b837b361fc..329c094011 100644 --- a/internal/config/bench.go +++ b/internal/config/benchmark.go @@ -67,6 +67,18 @@ const ( DefaultMethod = "GET" ) +// DefaultBenchSpec returns a default bench spec. +func DefaultBenchSpec() BenchConfig { + return BenchConfig{ + C: DefaultC, + N: DefaultN, + HTTP: HTTP{ + Method: DefaultMethod, + Path: "/", + }, + } +} + func newBenchmark() Benchmark { return Benchmark{ C: DefaultC, @@ -106,15 +118,3 @@ func (s *Bench) load(path string) error { return yaml.Unmarshal(f, &s) } - -// DefaultBenchSpec returns a default bench spec. -func DefaultBenchSpec() BenchConfig { - return BenchConfig{ - C: DefaultC, - N: DefaultN, - HTTP: HTTP{ - Method: DefaultMethod, - Path: "/", - }, - } -} diff --git a/internal/config/bench_test.go b/internal/config/benchmark_test.go similarity index 92% rename from internal/config/bench_test.go rename to internal/config/benchmark_test.go index d6c225db5e..7a4b54caa8 100644 --- a/internal/config/bench_test.go +++ b/internal/config/benchmark_test.go @@ -35,14 +35,14 @@ func TestBenchLoad(t *testing.T) { coCount int }{ "goodConfig": { - "testdata/b_good.yml", + "testdata/b_good.yaml", 2, 1000, 2, 0, }, "malformed": { - "testdata/b_toast.yml", + "testdata/b_toast.yaml", 1, 200, 0, @@ -103,7 +103,7 @@ func TestBenchServiceLoad(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - b, err := NewBench("testdata/b_good.yml") + b, err := NewBench("testdata/b_good.yaml") assert.Nil(t, err) assert.Equal(t, 2, len(b.Benchmarks.Services)) @@ -122,16 +122,16 @@ func TestBenchServiceLoad(t *testing.T) { } func TestBenchReLoad(t *testing.T) { - b, err := NewBench("testdata/b_containers.yml") + b, err := NewBench("testdata/b_containers.yaml") assert.Nil(t, err) assert.Equal(t, 2, b.Benchmarks.Defaults.C) - assert.Nil(t, b.Reload("testdata/b_containers_1.yml")) + assert.Nil(t, b.Reload("testdata/b_containers_1.yaml")) assert.Equal(t, 20, b.Benchmarks.Defaults.C) } func TestBenchLoadToast(t *testing.T) { - _, err := NewBench("testdata/toast.yml") + _, err := NewBench("testdata/toast.yaml") assert.NotNil(t, err) } @@ -174,7 +174,7 @@ func TestBenchContainerLoad(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - b, err := NewBench("testdata/b_containers.yml") + b, err := NewBench("testdata/b_containers.yaml") assert.Nil(t, err) assert.Equal(t, 2, len(b.Benchmarks.Services)) diff --git a/internal/config/cluster.go b/internal/config/cluster.go deleted file mode 100644 index 1fe5dccf9b..0000000000 --- a/internal/config/cluster.go +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package config - -import "github.com/derailed/k9s/internal/client" - -// DefaultPFAddress specifies the default PortForward host address. -const DefaultPFAddress = "localhost" - -// Cluster tracks K9s cluster configuration. -type Cluster struct { - Namespace *Namespace `yaml:"namespace"` - View *View `yaml:"view"` - Skin string `yaml:"skin,omitempty"` - FeatureGates *FeatureGates `yaml:"featureGates"` - PortForwardAddress string `yaml:"portForwardAddress"` -} - -// NewCluster creates a new cluster configuration. -func NewCluster() *Cluster { - return &Cluster{ - Namespace: NewNamespace(), - View: NewView(), - PortForwardAddress: DefaultPFAddress, - FeatureGates: NewFeatureGates(), - } -} - -// Validate a cluster config. -func (c *Cluster) Validate(conn client.Connection, ks KubeSettings) { - if c.PortForwardAddress == "" { - c.PortForwardAddress = DefaultPFAddress - } - - if c.Namespace == nil { - c.Namespace = NewNamespace() - } - if c.Namespace.Active == client.AllNamespaces { - c.Namespace.Active = client.NamespaceAll - } - - if c.FeatureGates == nil { - c.FeatureGates = NewFeatureGates() - } - - if c.View == nil { - c.View = NewView() - } - c.View.Validate() -} diff --git a/internal/config/cluster_test.go b/internal/config/cluster_test.go deleted file mode 100644 index 86f4957886..0000000000 --- a/internal/config/cluster_test.go +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package config_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestClusterValidate(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - - mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2", "default"}) - - c := config.NewCluster() - c.Validate(mc, mk) - - assert.Equal(t, "po", c.View.Active) - assert.Equal(t, "default", c.Namespace.Active) - assert.Equal(t, 1, len(c.Namespace.Favorites)) - assert.Equal(t, []string{"default"}, c.Namespace.Favorites) -} - -func TestClusterValidateEmpty(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - - mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2", "default"}) - - var c config.Cluster - c.Validate(mc, mk) - - assert.Equal(t, "po", c.View.Active) - assert.Equal(t, "default", c.Namespace.Active) - assert.Equal(t, 1, len(c.Namespace.Favorites)) - assert.Equal(t, []string{"default"}, c.Namespace.Favorites) -} - -func namespaces() []v1.Namespace { - return []v1.Namespace{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "default", - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "fred", - }, - }, - } -} diff --git a/internal/config/config.go b/internal/config/config.go index f7674c7777..d4e8e81b7a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,7 +4,6 @@ package config import ( - "errors" "fmt" "os" "path/filepath" @@ -12,56 +11,26 @@ import ( "github.com/adrg/xdg" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config/data" "github.com/rs/zerolog/log" "gopkg.in/yaml.v2" "k8s.io/cli-runtime/pkg/genericclioptions" ) -// K9sConfig represents K9s configuration dir env var. -const K9sConfig = "K9SCONFIG" - -var ( - // K9sConfigFile represents K9s config file location. - K9sConfigFile = filepath.Join(K9sHome(), "config.yml") - - // K9sSkinDir represent K9s skin dir - K9sSkinDir = filepath.Join(K9sHome(), "skins") - - // K9sDefaultScreenDumpDir represents a default directory where K9s screen dumps will be persisted. - K9sDefaultScreenDumpDir = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-screens-%s", MustK9sUser())) -) - -type ( - // KubeSettings exposes kubeconfig context information. - KubeSettings interface { - // CurrentContextName returns the name of the current context. - CurrentContextName() (string, error) - - // CurrentClusterName returns the name of the current cluster. - CurrentClusterName() (string, error) - - // CurrentNamespace returns the name of the current namespace. - CurrentNamespaceName() (string, error) - - // ClusterNames() returns all available cluster names. - ClusterNames() (map[string]struct{}, error) - } - - // Config tracks K9s configuration options. - Config struct { - K9s *K9s `yaml:"k9s"` - client client.Connection - settings KubeSettings - } -) +// Config tracks K9s configuration options. +type Config struct { + K9s *K9s `yaml:"k9s"` + conn client.Connection + settings data.KubeSettings +} // K9sHome returns k9s configs home directory. func K9sHome() string { - if env := os.Getenv(K9sConfig); env != "" { + if env := os.Getenv(K9sConfigDir); env != "" { return env } - xdgK9sHome, err := xdg.ConfigFile("k9s") + xdgK9sHome, err := xdg.ConfigFile(AppName) if err != nil { log.Fatal().Err(err).Msg("Unable to create configuration directory for k9s") } @@ -70,35 +39,50 @@ func K9sHome() string { } // NewConfig creates a new default config. -func NewConfig(ks KubeSettings) *Config { - return &Config{K9s: NewK9s(), settings: ks} +func NewConfig(ks data.KubeSettings) *Config { + return &Config{ + settings: ks, + K9s: NewK9s(nil, ks), + } +} + +// ContextAliasesPath returns a context specific aliases file spec. +func (c *Config) ContextAliasesPath() string { + ct, err := c.K9s.ActiveContext() + if err != nil { + return "" + } + + return AppContextAliasesFile(ct.ClusterName, c.K9s.activeContextName) +} + +// ContextPluginsPath returns a context specific plugins file spec. +func (c *Config) ContextPluginsPath() string { + ct, err := c.K9s.ActiveContext() + if err != nil { + return "" + } + + return AppContextPluginsFile(ct.ClusterName, c.K9s.activeContextName) } // Refine the configuration based on cli args. func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, cfg *client.Config) error { if isSet(flags.Context) { - c.K9s.CurrentContext = *flags.Context + if _, err := c.K9s.ActivateContext(*flags.Context); err != nil { + return err + } } else { - context, err := cfg.CurrentContextName() + n, err := cfg.CurrentContextName() + if err != nil { + return err + } + _, err = c.K9s.ActivateContext(n) if err != nil { return err } - c.K9s.CurrentContext = context - } - log.Debug().Msgf("Active Context %q", c.K9s.CurrentContext) - if c.K9s.CurrentContext == "" { - return errors.New("Invalid kubeconfig context detected") - } - cc, err := cfg.Contexts() - if err != nil { - return err - } - context, ok := cc[c.K9s.CurrentContext] - if !ok { - return fmt.Errorf("the specified context %q does not exists in kubeconfig", c.K9s.CurrentContext) } - c.K9s.CurrentCluster = context.Cluster - c.K9s.ActivateCluster(context.Namespace) + log.Debug().Msgf("Active Context %q", c.K9s.ActiveContextName()) var ns = client.DefaultNamespace switch { @@ -107,96 +91,87 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c case isSet(flags.Namespace): ns = *flags.Namespace default: - if nss := context.Namespace; nss != "" { - ns = nss - } else if nss == "" { - ns = c.K9s.ActiveCluster().Namespace.Active + nss, err := c.K9s.ActiveContextNamespace() + if err != nil { + return err } + ns = nss } - if err := c.SetActiveNamespace(ns); err != nil { return err } flags.Namespace = &ns - if isSet(flags.ClusterName) { - c.K9s.CurrentCluster = *flags.ClusterName - } - - return EnsureDirPath(c.K9s.GetScreenDumpDir(), DefaultDirMod) + return data.EnsureDirPath(c.K9s.GetScreenDumpDir(), data.DefaultDirMod) } -// Reset the context to the new current context/cluster. -// if it does not exist. +// Reset resets the context to the new current context/cluster. func (c *Config) Reset() { - c.K9s.CurrentContext, c.K9s.CurrentCluster = "", "" + c.K9s.Reset() } -// CurrentCluster fetch the configuration activeCluster. -func (c *Config) CurrentCluster() *Cluster { - if c, ok := c.K9s.Clusters[c.K9s.CurrentCluster]; ok { - return c +func (c *Config) SetCurrentContext(n string) (*data.Context, error) { + ct, err := c.K9s.ActivateContext(n) + if err != nil { + return nil, fmt.Errorf("set current context %q failed: %w", n, err) } - return nil + + return ct, nil +} + +// CurrentContext fetch the configuration active context. +func (c *Config) CurrentContext() (*data.Context, error) { + return c.K9s.ActiveContext() } -// ActiveNamespace returns the active namespace in the current cluster. +// ActiveNamespace returns the active namespace in the current context. +// If none found return the empty ns. func (c *Config) ActiveNamespace() string { - if c.K9s.Clusters == nil { - log.Warn().Msgf("No context detected returning default namespace") - return "default" - } - cl := c.CurrentCluster() - if cl != nil && cl.Namespace != nil { - return cl.Namespace.Active - } - if cl == nil { - cl = NewCluster() - c.K9s.Clusters[c.K9s.CurrentCluster] = cl - } - if ns, err := c.settings.CurrentNamespaceName(); err == nil && ns != "" { - if cl.Namespace == nil { - cl.Namespace = NewNamespace() - } - cl.Namespace.Active = ns - return ns + ns, err := c.K9s.ActiveContextNamespace() + if err != nil { + log.Error().Err(err).Msgf("Unable to assert active namespace. Using default") + ns = client.DefaultNamespace } - return "default" + return ns } // ValidateFavorites ensure favorite ns are legit. func (c *Config) ValidateFavorites() { - cl := c.K9s.ActiveCluster() - cl.Validate(c.client, c.settings) - cl.Namespace.Validate(c.client, c.settings) + ct, err := c.K9s.ActiveContext() + if err == nil { + ct.Validate(c.conn, c.settings) + ct.Namespace.Validate(c.conn, c.settings) + } } -// FavNamespaces returns fav namespaces in the current cluster. +// FavNamespaces returns fav namespaces in the current context. func (c *Config) FavNamespaces() []string { - cl := c.K9s.ActiveCluster() + ct, err := c.K9s.ActiveContext() + if err != nil { + return nil + } - return cl.Namespace.Favorites + return ct.Namespace.Favorites } -// SetActiveNamespace set the active namespace in the current cluster. +// SetActiveNamespace set the active namespace in the current context. func (c *Config) SetActiveNamespace(ns string) error { - if cl := c.K9s.ActiveCluster(); cl != nil { - return cl.Namespace.SetActive(ns, c.settings) + ct, err := c.K9s.ActiveContext() + if err != nil { + return err } - err := errors.New("no active cluster. unable to set active namespace") - log.Error().Err(err).Msg("SetActiveNamespace") - return err + return ct.Namespace.SetActive(ns, c.settings) } -// ActiveView returns the active view in the current cluster. +// ActiveView returns the active view in the current context. func (c *Config) ActiveView() string { - cl := c.K9s.ActiveCluster() - if cl == nil { - return defaultView + ct, err := c.K9s.ActiveContext() + if err != nil { + return data.DefaultView } - cmd := cl.View.Active + cmd := ct.View.Active if c.K9s.manualCommand != nil && *c.K9s.manualCommand != "" { cmd = *c.K9s.manualCommand // We reset the manualCommand property because @@ -208,37 +183,41 @@ func (c *Config) ActiveView() string { return cmd } -// SetActiveView set the currently cluster active view. +// SetActiveView sets current context active view. func (c *Config) SetActiveView(view string) { - if cl := c.K9s.ActiveCluster(); cl != nil { - cl.View.Active = view + if ct, err := c.K9s.ActiveContext(); err == nil { + ct.View.Active = view } } // GetConnection return an api server connection. func (c *Config) GetConnection() client.Connection { - return c.client + return c.conn } // SetConnection set an api server connection. func (c *Config) SetConnection(conn client.Connection) { - c.client = conn + c.conn, c.K9s.conn = conn, conn + c.Validate() +} + +func (c *Config) ActiveContextName() string { + return c.K9s.activeContextName } -// Load K9s configuration from file. +// Load loads K9s configuration from file. func (c *Config) Load(path string) error { f, err := os.ReadFile(path) if err != nil { return err } - c.K9s = NewK9s() var cfg Config if err := yaml.Unmarshal(f, &cfg); err != nil { return err } if cfg.K9s != nil { - c.K9s = cfg.K9s + c.K9s.Refine(cfg.K9s) } if c.K9s.Logger == nil { c.K9s.Logger = NewLogger() @@ -249,13 +228,15 @@ func (c *Config) Load(path string) error { // Save configuration to disk. func (c *Config) Save() error { c.Validate() - - return c.SaveFile(K9sConfigFile) + if err := c.K9s.Save(); err != nil { + return err + } + return c.SaveFile(AppConfigFile) } // SaveFile K9s configuration to disk. func (c *Config) SaveFile(path string) error { - if err := EnsureDirPath(path, DefaultDirMod); err != nil { + if err := data.EnsureDirPath(path, data.DefaultDirMod); err != nil { return err } cfg, err := yaml.Marshal(c) @@ -268,14 +249,14 @@ func (c *Config) SaveFile(path string) error { // Validate the configuration. func (c *Config) Validate() { - c.K9s.Validate(c.client, c.settings) + c.K9s.Validate(c.conn, c.settings) } // Dump debug... func (c *Config) Dump(msg string) { - log.Debug().Msgf("Current Cluster: %s\n", c.K9s.CurrentCluster) - for k, cl := range c.K9s.Clusters { - log.Debug().Msgf("K9s cluster: %s -- %+v\n", k, cl.Namespace) + ct, err := c.K9s.ActiveContext() + if err != nil { + log.Debug().Msgf("Current Contexts: %s\n", ct.ClusterName) } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index bef14aeab5..e29d9f7add 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -10,7 +10,7 @@ import ( "testing" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" m "github.com/petergtz/pegomock" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" @@ -22,19 +22,16 @@ func init() { } func TestConfigRefine(t *testing.T) { - cfgFile, ctx, cluster, ns := "testdata/kubeconfig-test.yml", "test2", "cluster2", "ns2" + var ( + cfgFile = "testdata/kubeconfig-test.yaml" + ctx, cluster, ns = "ct-1-1", "cl-1", "ns-1" + ) + uu := map[string]struct { flags *genericclioptions.ConfigFlags issue bool context, cluster, namespace string }{ - "plain": { - flags: &genericclioptions.ConfigFlags{KubeConfig: &cfgFile}, - issue: false, - context: "test1", - cluster: "cluster1", - namespace: "ns1", - }, "overrideNS": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, @@ -61,18 +58,14 @@ func TestConfigRefine(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - mk := newMockSettings(u.flags) - cfg := config.NewConfig(mk) + cfg := mock.NewMockConfig() err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags)) if u.issue { assert.NotNil(t, err) } else { assert.Nil(t, err) - assert.Equal(t, u.context, cfg.K9s.CurrentContext) - assert.Equal(t, u.cluster, cfg.K9s.CurrentCluster) + assert.Equal(t, u.context, cfg.K9s.ActiveContextName()) assert.Equal(t, u.namespace, cfg.ActiveNamespace()) } }) @@ -80,167 +73,60 @@ func TestConfigRefine(t *testing.T) { } func TestConfigValidate(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - - mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"}) + cfg := mock.NewMockConfig() + cfg.SetConnection(mock.NewMockConnection()) - cfg := config.NewConfig(mk) - cfg.SetConnection(mc) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) + assert.Nil(t, cfg.Load("testdata/k9s.yaml")) cfg.Validate() } func TestConfigLoad(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) + cfg := mock.NewMockConfig() + assert.Nil(t, cfg.Load("testdata/k9s.yaml")) assert.Equal(t, 2, cfg.K9s.RefreshRate) assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize) assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount) - assert.Equal(t, "minikube", cfg.K9s.CurrentContext) - assert.Equal(t, "minikube", cfg.K9s.CurrentCluster) - assert.NotNil(t, cfg.K9s.Clusters) - assert.Equal(t, 2, len(cfg.K9s.Clusters)) - - nn := []string{ - "default", - "kube-public", - "istio-system", - "all", - "kube-system", - } - - assert.Equal(t, "kube-system", cfg.K9s.Clusters["minikube"].Namespace.Active) - assert.Equal(t, nn, cfg.K9s.Clusters["minikube"].Namespace.Favorites) - assert.Equal(t, "ctx", cfg.K9s.Clusters["minikube"].View.Active) -} - -func TestConfigCurrentCluster(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - assert.NotNil(t, cfg.CurrentCluster()) - assert.Equal(t, "kube-system", cfg.CurrentCluster().Namespace.Active) - assert.Equal(t, "ctx", cfg.CurrentCluster().View.Active) -} - -func TestConfigActiveNamespace(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - assert.Equal(t, "kube-system", cfg.ActiveNamespace()) -} - -func TestConfigActiveNamespaceBlank(t *testing.T) { - cfg := config.Config{K9s: new(config.K9s)} - assert.Equal(t, "default", cfg.ActiveNamespace()) -} - -func TestConfigSetActiveNamespace(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - assert.Nil(t, cfg.SetActiveNamespace("default")) - assert.Equal(t, "default", cfg.ActiveNamespace()) -} - -func TestConfigActiveView(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - assert.Equal(t, "ctx", cfg.ActiveView()) -} - -func TestConfigActiveViewBlank(t *testing.T) { - cfg := config.Config{K9s: new(config.K9s)} - assert.Equal(t, "po", cfg.ActiveView()) -} - -func TestConfigSetActiveView(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - cfg.SetActiveView("po") - assert.Equal(t, "po", cfg.ActiveView()) -} - -func TestConfigFavNamespaces(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - expectedNS := []string{"default", "kube-public", "istio-system", "all", "kube-system"} - assert.Equal(t, expectedNS, cfg.FavNamespaces()) } func TestConfigLoadOldCfg(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s_old.yml")) + cfg := mock.NewMockConfig() + + assert.Nil(t, cfg.Load("testdata/k9s_old.yaml")) } func TestConfigLoadCrap(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.NotNil(t, cfg.Load("testdata/k9s_not_there.yml")) + cfg := mock.NewMockConfig() + + assert.NotNil(t, cfg.Load("testdata/k9s_not_there.yaml")) } func TestConfigSaveFile(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) + cfg := mock.NewMockConfig() - mk := NewMockKubeSettings() - m.When(mk.CurrentContextName()).ThenReturn("minikube", nil) - m.When(mk.CurrentClusterName()).ThenReturn("minikube", nil) - m.When(mk.CurrentNamespaceName()).ThenReturn("default", nil) - m.When(mk.ClusterNames()).ThenReturn(map[string]struct{}{"minikube": {}, "fred": {}, "blee": {}}, nil) - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"}) + assert.Nil(t, cfg.Load("testdata/k9s.yaml")) - cfg := config.NewConfig(mk) - cfg.SetConnection(mc) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) cfg.K9s.RefreshRate = 100 cfg.K9s.ReadOnly = true cfg.K9s.Logger.TailCount = 500 cfg.K9s.Logger.BufferSize = 800 - cfg.K9s.CurrentContext = "blee" - cfg.K9s.CurrentCluster = "blee" cfg.Validate() - path := filepath.Join("/tmp", "k9s.yml") + + path := filepath.Join("/tmp", "k9s.yaml") err := cfg.SaveFile(path) assert.Nil(t, err) - raw, err := os.ReadFile(path) assert.Nil(t, err) assert.Equal(t, expectedConfig, string(raw)) } func TestConfigReset(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - - mk := NewMockKubeSettings() - m.When(mk.CurrentContextName()).ThenReturn("blee", nil) - m.When(mk.CurrentClusterName()).ThenReturn("blee", nil) - m.When(mk.CurrentNamespaceName()).ThenReturn("default", nil) - m.When(mk.ClusterNames()).ThenReturn(map[string]struct{}{"blee": {}}, nil) - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"}) - - cfg := config.NewConfig(mk) - cfg.SetConnection(mc) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) + cfg := mock.NewMockConfig() + assert.Nil(t, cfg.Load("testdata/k9s.yaml")) cfg.Reset() cfg.Validate() - path := filepath.Join("/tmp", "k9s.yml") + path := filepath.Join("/tmp", "k9s.yaml") err := cfg.SaveFile(path) assert.Nil(t, err) @@ -258,46 +144,35 @@ func TestSetup(t *testing.T) { }) } -type mockSettings struct { - flags *genericclioptions.ConfigFlags -} - -var _ config.KubeSettings = (*mockSettings)(nil) - -func newMockSettings(flags *genericclioptions.ConfigFlags) *mockSettings { - return &mockSettings{flags: flags} -} -func (m *mockSettings) CurrentContextName() (string, error) { - return *m.flags.Context, nil -} -func (m *mockSettings) CurrentClusterName() (string, error) { return "", nil } -func (m *mockSettings) CurrentNamespaceName() (string, error) { - return *m.flags.Namespace, nil -} -func (m *mockSettings) ClusterNames() (map[string]struct{}, error) { return nil, nil } - // ---------------------------------------------------------------------------- // Test Data... var expectedConfig = `k9s: liveViewAutoRefresh: true + screenDumpDir: /tmp refreshRate: 100 maxConnRetry: 5 - enableMouse: false - enableImageScan: false - headless: false - logoless: false - crumbsless: false readOnly: true noExitOnCtrlC: false - noIcons: false + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + noIcons: false + skipLatestRevCheck: false + disablePodCounting: false shellPod: image: busybox:1.35.0 namespace: default limits: cpu: 100m memory: 100Mi - skipLatestRevCheck: false + imageScans: + enable: false + blackList: + namespaces: [] + labels: {} logger: tail: 500 buffer: 800 @@ -305,51 +180,6 @@ var expectedConfig = `k9s: fullScreenLogs: false textWrap: false showTime: false - currentContext: blee - currentCluster: blee - keepMissingClusters: false - clusters: - blee: - namespace: - active: default - lockFavorites: false - favorites: - - default - view: - active: po - featureGates: - nodeShell: false - portForwardAddress: localhost - fred: - namespace: - active: default - lockFavorites: false - favorites: - - default - - kube-public - - istio-system - - all - - kube-system - view: - active: po - featureGates: - nodeShell: false - portForwardAddress: localhost - minikube: - namespace: - active: kube-system - lockFavorites: false - favorites: - - default - - kube-public - - istio-system - - all - - kube-system - view: - active: ctx - featureGates: - nodeShell: false - portForwardAddress: localhost thresholds: cpu: critical: 90 @@ -357,29 +187,34 @@ var expectedConfig = `k9s: memory: critical: 90 warn: 70 - screenDumpDir: /tmp - disablePodCounting: false ` var resetConfig = `k9s: liveViewAutoRefresh: true + screenDumpDir: /tmp refreshRate: 2 maxConnRetry: 5 - enableMouse: false - enableImageScan: false - headless: false - logoless: false - crumbsless: false readOnly: false noExitOnCtrlC: false - noIcons: false + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + noIcons: false + skipLatestRevCheck: false + disablePodCounting: false shellPod: image: busybox:1.35.0 namespace: default limits: cpu: 100m memory: 100Mi - skipLatestRevCheck: false + imageScans: + enable: false + blackList: + namespaces: [] + labels: {} logger: tail: 200 buffer: 2000 @@ -387,21 +222,6 @@ var resetConfig = `k9s: fullScreenLogs: false textWrap: false showTime: false - currentContext: blee - currentCluster: blee - keepMissingClusters: false - clusters: - blee: - namespace: - active: default - lockFavorites: false - favorites: - - default - view: - active: po - featureGates: - nodeShell: false - portForwardAddress: localhost thresholds: cpu: critical: 90 @@ -409,6 +229,4 @@ var resetConfig = `k9s: memory: critical: 90 warn: 70 - screenDumpDir: /tmp - disablePodCounting: false ` diff --git a/internal/config/data/config.go b/internal/config/data/config.go new file mode 100644 index 0000000000..3faf5fcc27 --- /dev/null +++ b/internal/config/data/config.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +import ( + "fmt" + "io" + "os" + + "github.com/derailed/k9s/internal/client" + "gopkg.in/yaml.v2" + "k8s.io/client-go/tools/clientcmd/api" +) + +// Config tracks a context configuration. +type Config struct { + Context *Context `yaml:"k9s"` +} + +func NewConfig(ct *api.Context) *Config { + return &Config{ + Context: NewContextFromConfig(ct), + } +} + +func (c *Config) Validate(conn client.Connection, ks KubeSettings) { + c.Context.Validate(conn, ks) +} + +func (c *Config) Dump(w io.Writer) { + bb, _ := yaml.Marshal(&c) + + fmt.Fprintf(w, "%s\n", string(bb)) +} + +func (c *Config) Save(path string) error { + if err := EnsureDirPath(path, DefaultDirMod); err != nil { + return err + } + + cfg, err := yaml.Marshal(c) + if err != nil { + return err + } + + return os.WriteFile(path, cfg, DefaultFileMod) +} diff --git a/internal/config/data/context.go b/internal/config/data/context.go new file mode 100644 index 0000000000..e08de8ffa2 --- /dev/null +++ b/internal/config/data/context.go @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +import ( + "github.com/derailed/k9s/internal/client" + "k8s.io/client-go/tools/clientcmd/api" +) + +// DefaultPFAddress specifies the default PortForward host address. +const DefaultPFAddress = "localhost" + +// Context tracks K9s context configuration. +type Context struct { + ClusterName string `yaml:"cluster,omitempty"` + ReadOnly bool `yaml:"readOnly"` + Skin string `yaml:"skin,omitempty"` + Namespace *Namespace `yaml:"namespace"` + View *View `yaml:"view"` + FeatureGates FeatureGates `yaml:"featureGates"` + PortForwardAddress string `yaml:"portForwardAddress"` +} + +// NewContext creates a new cluster configuration. +func NewContext() *Context { + return &Context{ + Namespace: NewNamespace(), + View: NewView(), + PortForwardAddress: DefaultPFAddress, + FeatureGates: NewFeatureGates(), + } +} + +func NewContextFromConfig(cfg *api.Context) *Context { + return &Context{ + Namespace: NewActiveNamespace(cfg.Namespace), + ClusterName: cfg.Cluster, + View: NewView(), + PortForwardAddress: DefaultPFAddress, + FeatureGates: NewFeatureGates(), + } +} + +// Validate a context config. +func (c *Context) Validate(conn client.Connection, ks KubeSettings) { + if c.PortForwardAddress == "" { + c.PortForwardAddress = DefaultPFAddress + } + + if cl, err := ks.CurrentClusterName(); err != nil { + c.ClusterName = cl + } + + if c.Namespace == nil { + c.Namespace = NewNamespace() + } + if c.Namespace.Active == client.BlankNamespace { + c.Namespace.Active = client.DefaultNamespace + } + c.Namespace.Validate(conn, ks) + + if c.View == nil { + c.View = NewView() + } + c.View.Validate() +} diff --git a/internal/config/data/context_test.go b/internal/config/data/context_test.go new file mode 100644 index 0000000000..d66c8d5689 --- /dev/null +++ b/internal/config/data/context_test.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/mock" + "github.com/stretchr/testify/assert" +) + +func TestClusterValidate(t *testing.T) { + c := data.NewContext() + c.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1"))) + + assert.Equal(t, "po", c.View.Active) + assert.Equal(t, "default", c.Namespace.Active) + assert.Equal(t, 1, len(c.Namespace.Favorites)) + assert.Equal(t, []string{"default"}, c.Namespace.Favorites) +} + +func TestClusterValidateEmpty(t *testing.T) { + c := data.NewContext() + c.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1"))) + + assert.Equal(t, "po", c.View.Active) + assert.Equal(t, "default", c.Namespace.Active) + assert.Equal(t, 1, len(c.Namespace.Favorites)) + assert.Equal(t, []string{"default"}, c.Namespace.Favorites) +} diff --git a/internal/config/data/dir.go b/internal/config/data/dir.go new file mode 100644 index 0000000000..3a577eceda --- /dev/null +++ b/internal/config/data/dir.go @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +import ( + "errors" + "os" + "path/filepath" + + "github.com/derailed/k9s/internal/client" + "github.com/rs/zerolog/log" + "gopkg.in/yaml.v2" + "k8s.io/client-go/tools/clientcmd/api" +) + +type Dir struct { + root string + conn client.Connection + ks KubeSettings +} + +func NewDir(root string, conn client.Connection, ks KubeSettings) *Dir { + return &Dir{ + root: root, + ks: ks, + conn: conn, + } +} + +func (d Dir) Load(n string, ct *api.Context) (*Config, error) { + if ct == nil { + return nil, errors.New("api.Context must not be nil") + } + + var ( + path = filepath.Join(d.root, ct.Cluster, n, MainConfigFile) + cfg *Config + err error + ) + if f, e := os.Stat(path); os.IsNotExist(e) || f.Size() == 0 { + log.Debug().Msgf("Context config not found! Generating... %q", path) + cfg, err = d.genConfig(path, ct) + } else { + log.Debug().Msgf("Found existing context config: %q", path) + cfg, err = d.loadConfig(path) + } + + return cfg, err +} + +func (d *Dir) genConfig(path string, ct *api.Context) (*Config, error) { + cfg := NewConfig(ct) + cfg.Validate(d.conn, d.ks) + if err := cfg.Save(path); err != nil { + return nil, err + } + + return cfg, nil +} + +func (d *Dir) loadConfig(path string) (*Config, error) { + bb, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var cfg Config + if err := yaml.Unmarshal(bb, &cfg); err != nil { + return nil, err + } + cfg.Validate(d.conn, d.ks) + + return &cfg, nil +} diff --git a/internal/config/data/dir_test.go b/internal/config/data/dir_test.go new file mode 100644 index 0000000000..fc4c3d3fde --- /dev/null +++ b/internal/config/data/dir_test.go @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data_test + +import ( + "os" + "strings" + "testing" + + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/mock" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestDirLoad(t *testing.T) { + uu := map[string]struct { + dir string + flags *genericclioptions.ConfigFlags + err error + cfg *data.Config + }{ + "happy-cl-1-ct-1": { + dir: "testdata/data/k9s", + flags: makeFlags("cl-1", "ct-1-1"), + cfg: mustLoadConfig("testdata/configs/ct-1-1.yaml"), + }, + + "happy-cl-1-ct2": { + dir: "testdata/data/k9s", + flags: makeFlags("cl-1", "ct-1-2"), + cfg: mustLoadConfig("testdata/configs/ct-1-2.yaml"), + }, + + "happy-cl-2": { + dir: "testdata/data/k9s", + flags: makeFlags("cl-2", "ct-2-1"), + cfg: mustLoadConfig("testdata/configs/ct-2-1.yaml"), + }, + + "toast": { + dir: "/tmp/data/k9s", + flags: makeFlags("cl-test", "ct-test-1"), + cfg: mustLoadConfig("testdata/configs/def_ct.yaml"), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.NotNil(t, u.cfg, "test config must not be nil") + if u.cfg == nil { + return + } + + ks := mock.NewMockKubeSettings(u.flags) + if strings.Index(u.dir, "/tmp") == 0 { + assert.NoError(t, mock.EnsureDir(u.dir)) + } + + d := data.NewDir(u.dir, mock.NewMockConnection(), ks) + ct, err := ks.CurrentContext() + assert.NoError(t, err) + if err != nil { + return + } + + cfg, err := d.Load(*u.flags.Context, ct) + assert.Equal(t, u.err, err) + if u.err == nil { + assert.Equal(t, u.cfg, cfg) + } + }) + } +} + +// Helpers... + +func makeFlags(cl, ct string) *genericclioptions.ConfigFlags { + return &genericclioptions.ConfigFlags{ + ClusterName: &cl, + Context: &ct, + } +} + +func mustLoadConfig(cfg string) *data.Config { + bb, err := os.ReadFile(cfg) + if err != nil { + return nil + } + var ct data.Config + if err = yaml.Unmarshal(bb, &ct); err != nil { + return nil + } + + return &ct +} diff --git a/internal/config/data/feature_gate.go b/internal/config/data/feature_gate.go new file mode 100644 index 0000000000..8631c309a8 --- /dev/null +++ b/internal/config/data/feature_gate.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +// FeatureGates represents K9s opt-in features. +type FeatureGates struct { + NodeShell bool `yaml:"nodeShell"` +} + +// NewFeatureGates returns a new feature gate. +func NewFeatureGates() FeatureGates { + return FeatureGates{} +} diff --git a/internal/config/data/helpers.go b/internal/config/data/helpers.go new file mode 100644 index 0000000000..98887bf6dd --- /dev/null +++ b/internal/config/data/helpers.go @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +import ( + "os" + "path/filepath" +) + +// InList check if string is in a collection of strings. +func InList(ll []string, n string) bool { + for _, l := range ll { + if l == n { + return true + } + } + return false +} + +// EnsureDirPath ensures a directory exist from the given path. +func EnsureDirPath(path string, mod os.FileMode) error { + return EnsureFullPath(filepath.Dir(path), mod) +} + +// EnsureFullPath ensures a directory exist from the given path. +func EnsureFullPath(path string, mod os.FileMode) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + if err = os.MkdirAll(path, mod); err != nil { + return err + } + } + + return nil +} diff --git a/internal/config/data/helpers_test.go b/internal/config/data/helpers_test.go new file mode 100644 index 0000000000..be4a6a8537 --- /dev/null +++ b/internal/config/data/helpers_test.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/derailed/k9s/internal/config/data" + "github.com/stretchr/testify/assert" +) + +func TestHelperInList(t *testing.T) { + uu := []struct { + item string + list []string + expected bool + }{ + {"a", []string{}, false}, + {"", []string{}, false}, + {"", []string{""}, true}, + {"a", []string{"a", "b", "c", "d"}, true}, + {"z", []string{"a", "b", "c", "d"}, false}, + } + + for _, u := range uu { + assert.Equal(t, u.expected, data.InList(u.list, u.item)) + } +} + +func TestEnsureDirPathNone(t *testing.T) { + var mod os.FileMode = 0744 + dir := filepath.Join("/tmp", "fred") + os.Remove(dir) + + path := filepath.Join(dir, "duh.yaml") + assert.NoError(t, data.EnsureDirPath(path, mod)) + + p, err := os.Stat(dir) + assert.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") + os.Remove(dir) + assert.NoError(t, os.Mkdir(dir, mod)) + + path := filepath.Join(dir, "duh.yaml") + assert.NoError(t, data.EnsureDirPath(path, mod)) + + p, err := os.Stat(dir) + assert.NoError(t, err) + assert.Equal(t, "drwxr--r--", p.Mode().String()) +} diff --git a/internal/config/ns.go b/internal/config/data/ns.go similarity index 74% rename from internal/config/ns.go rename to internal/config/data/ns.go index c49f4324b1..252b4f5750 100644 --- a/internal/config/ns.go +++ b/internal/config/data/ns.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s -package config +package data import ( "github.com/derailed/k9s/internal/client" @@ -11,8 +11,6 @@ import ( const ( // MaxFavoritesNS number # favorite namespaces to keep in the configuration. MaxFavoritesNS = 9 - defaultNS = "default" - allNS = "all" ) // Namespace tracks active and favorites namespaces. @@ -25,27 +23,33 @@ type Namespace struct { // NewNamespace create a new namespace configuration. func NewNamespace() *Namespace { return &Namespace{ - Active: defaultNS, - Favorites: []string{defaultNS}, + Active: client.DefaultNamespace, + Favorites: []string{client.DefaultNamespace}, + } +} + +func NewActiveNamespace(n string) *Namespace { + return &Namespace{ + Active: n, + Favorites: []string{client.DefaultNamespace}, } } // Validate a namespace is setup correctly. func (n *Namespace) Validate(c client.Connection, ks KubeSettings) { if c == nil { - return + n = NewActiveNamespace(client.DefaultNamespace) } - nns, err := c.ValidNamespaces() - if err != nil { + if c == nil { + log.Debug().Msgf("No connection found. Skipping ns validation") return } - nn := client.NamespaceNames(nns) - if !n.isAllNamespaces() && !InList(nn, n.Active) { + if !n.isAllNamespaces() && !c.IsValidNamespace(n.Active) { log.Error().Msgf("[Config] Validation error active namespace %q does not exists", n.Active) } for _, ns := range n.Favorites { - if ns != allNS && !InList(nn, ns) { + if ns != client.NamespaceAll && !c.IsValidNamespace(ns) { log.Debug().Msgf("[Config] Invalid favorite found '%s' - %t", ns, n.isAllNamespaces()) n.rmFavNS(ns) } @@ -54,8 +58,8 @@ func (n *Namespace) Validate(c client.Connection, ks KubeSettings) { // SetActive set the active namespace. func (n *Namespace) SetActive(ns string, ks KubeSettings) error { - if ns == client.NotNamespaced { - ns = client.AllNamespaces + if ns == client.BlankNamespace { + ns = client.NamespaceAll } n.Active = ns if ns != "" && !n.LockFavorites { @@ -66,7 +70,7 @@ func (n *Namespace) SetActive(ns string, ks KubeSettings) error { } func (n *Namespace) isAllNamespaces() bool { - return n.Active == allNS || n.Active == "" + return n.Active == client.NamespaceAll || n.Active == "" } func (n *Namespace) addFavNS(ns string) { diff --git a/internal/config/data/ns_test.go b/internal/config/data/ns_test.go new file mode 100644 index 0000000000..7a45ead59c --- /dev/null +++ b/internal/config/data/ns_test.go @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/mock" + "github.com/stretchr/testify/assert" +) + +func TestNSValidate(t *testing.T) { + ns := data.NewNamespace() + ns.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1"))) + + assert.Equal(t, "default", ns.Active) + assert.Equal(t, []string{"default"}, ns.Favorites) +} + +func TestNSValidateMissing(t *testing.T) { + ns := data.NewNamespace() + ns.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1"))) + + assert.Equal(t, "default", ns.Active) + assert.Equal(t, []string{"default"}, ns.Favorites) +} + +func TestNSValidateNoNS(t *testing.T) { + ns := data.NewNamespace() + ns.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1"))) + + assert.Equal(t, "default", ns.Active) + assert.Equal(t, []string{"default"}, ns.Favorites) +} + +func TestNSSetActive(t *testing.T) { + allNS := []string{"ns4", "ns3", "ns2", "ns1", "all", "default"} + uu := []struct { + ns string + fav []string + }{ + {"all", []string{"all", "default"}}, + {"ns1", []string{"ns1", "all", "default"}}, + {"ns2", []string{"ns2", "ns1", "all", "default"}}, + {"ns3", []string{"ns3", "ns2", "ns1", "all", "default"}}, + {"ns4", allNS}, + } + + mk := mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1")) + ns := data.NewNamespace() + for _, u := range uu { + err := ns.SetActive(u.ns, mk) + assert.Nil(t, err) + assert.Equal(t, u.ns, ns.Active) + assert.Equal(t, u.fav, ns.Favorites) + } +} + +func TestNSValidateRmFavs(t *testing.T) { + ns := data.NewNamespace() + ns.Favorites = []string{"default", "fred"} + ns.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1"))) + + assert.Equal(t, []string{"default", "fred"}, ns.Favorites) +} diff --git a/internal/config/data/testdata/configs/ct-1-1.yaml b/internal/config/data/testdata/configs/ct-1-1.yaml new file mode 100644 index 0000000000..091b90719b --- /dev/null +++ b/internal/config/data/testdata/configs/ct-1-1.yaml @@ -0,0 +1,16 @@ +k9s: + cluster: cl-1 + skin: skin-1 + readOnly: false + namespace: + active: ns-1 + lockFavorites: true + favorites: + - default + - ns-1 + - ns-2 + view: + active: dp + featureGates: + nodeShell: true + portForwardAddress: localhost diff --git a/internal/config/data/testdata/configs/ct-1-2.yaml b/internal/config/data/testdata/configs/ct-1-2.yaml new file mode 100644 index 0000000000..e7bd28f509 --- /dev/null +++ b/internal/config/data/testdata/configs/ct-1-2.yaml @@ -0,0 +1,14 @@ +k9s: + cluster: cl-1 + skin: in_the_navy + readOnly: true + namespace: + active: default + lockFavorites: false + favorites: + - default + view: + active: po + featureGates: + nodeShell: false + portForwardAddress: localhost diff --git a/internal/config/data/testdata/configs/ct-2-1.yaml b/internal/config/data/testdata/configs/ct-2-1.yaml new file mode 100644 index 0000000000..5a2e25befe --- /dev/null +++ b/internal/config/data/testdata/configs/ct-2-1.yaml @@ -0,0 +1,15 @@ +k9s: + cluster: cl-2 + skin: skin-2 + readOnly: true + namespace: + active: ns-2 + lockFavorites: true + favorites: + - ns-1 + - ns-2 + view: + active: svc + featureGates: + nodeShell: true + portForwardAddress: fred diff --git a/internal/config/data/testdata/configs/def_ct.yaml b/internal/config/data/testdata/configs/def_ct.yaml new file mode 100644 index 0000000000..a69eb34685 --- /dev/null +++ b/internal/config/data/testdata/configs/def_ct.yaml @@ -0,0 +1,12 @@ +k9s: + cluster: cl-test + namespace: + active: default + lockFavorites: false + favorites: + - default + view: + active: po + featureGates: + nodeShell: false + portForwardAddress: localhost diff --git a/internal/config/data/testdata/data/k9s/cl-1/ct-1-1/config.yaml b/internal/config/data/testdata/data/k9s/cl-1/ct-1-1/config.yaml new file mode 100644 index 0000000000..091b90719b --- /dev/null +++ b/internal/config/data/testdata/data/k9s/cl-1/ct-1-1/config.yaml @@ -0,0 +1,16 @@ +k9s: + cluster: cl-1 + skin: skin-1 + readOnly: false + namespace: + active: ns-1 + lockFavorites: true + favorites: + - default + - ns-1 + - ns-2 + view: + active: dp + featureGates: + nodeShell: true + portForwardAddress: localhost diff --git a/internal/config/data/testdata/data/k9s/cl-1/ct-1-2/config.yaml b/internal/config/data/testdata/data/k9s/cl-1/ct-1-2/config.yaml new file mode 100644 index 0000000000..e7bd28f509 --- /dev/null +++ b/internal/config/data/testdata/data/k9s/cl-1/ct-1-2/config.yaml @@ -0,0 +1,14 @@ +k9s: + cluster: cl-1 + skin: in_the_navy + readOnly: true + namespace: + active: default + lockFavorites: false + favorites: + - default + view: + active: po + featureGates: + nodeShell: false + portForwardAddress: localhost diff --git a/internal/config/data/testdata/data/k9s/cl-2/ct-2-1/config.yaml b/internal/config/data/testdata/data/k9s/cl-2/ct-2-1/config.yaml new file mode 100644 index 0000000000..5a2e25befe --- /dev/null +++ b/internal/config/data/testdata/data/k9s/cl-2/ct-2-1/config.yaml @@ -0,0 +1,15 @@ +k9s: + cluster: cl-2 + skin: skin-2 + readOnly: true + namespace: + active: ns-2 + lockFavorites: true + favorites: + - ns-1 + - ns-2 + view: + active: svc + featureGates: + nodeShell: true + portForwardAddress: fred diff --git a/internal/config/data/types.go b/internal/config/data/types.go new file mode 100644 index 0000000000..d798f77b8d --- /dev/null +++ b/internal/config/data/types.go @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +import ( + "os" + + "k8s.io/client-go/tools/clientcmd/api" +) + +const ( + // DefaultDirMod default unix perms for k9s directory. + DefaultDirMod os.FileMode = 0744 + + // DefaultFileMod default unix perms for k9s files. + DefaultFileMod os.FileMode = 0600 + + // MainConfigFile track main configuration file.. + MainConfigFile = "config.yaml" +) + +// KubeSettings exposes kubeconfig context information. +type KubeSettings interface { + // CurrentContextName returns the name of the current context. + CurrentContextName() (string, error) + + // CurrentClusterName returns the name of the current cluster. + CurrentClusterName() (string, error) + + // CurrentNamespace returns the name of the current namespace. + CurrentNamespaceName() (string, error) + + // ContextNames() returns all available context names. + ContextNames() (map[string]struct{}, error) + + // CurrentContext returns the current context configuration. + CurrentContext() (*api.Context, error) + + // GetContext returns a given context configuration or err if not found. + GetContext(string) (*api.Context, error) +} diff --git a/internal/config/view.go b/internal/config/data/view.go similarity index 76% rename from internal/config/view.go rename to internal/config/data/view.go index e4078ccdbe..044972ebe5 100644 --- a/internal/config/view.go +++ b/internal/config/data/view.go @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s -package config +package data -const defaultView = "po" +const DefaultView = "po" // View tracks view configuration options. type View struct { @@ -12,12 +12,12 @@ type View struct { // NewView creates a new view configuration. func NewView() *View { - return &View{Active: defaultView} + return &View{Active: DefaultView} } // Validate a view configuration. func (v *View) Validate() { if len(v.Active) == 0 { - v.Active = defaultView + v.Active = DefaultView } } diff --git a/internal/config/view_test.go b/internal/config/data/view_test.go similarity index 78% rename from internal/config/view_test.go rename to internal/config/data/view_test.go index 10dfb152e8..100491fef6 100644 --- a/internal/config/view_test.go +++ b/internal/config/data/view_test.go @@ -1,17 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s -package config_test +package data_test import ( "testing" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" "github.com/stretchr/testify/assert" ) func TestViewValidate(t *testing.T) { - v := config.NewView() + v := data.NewView() v.Validate() assert.Equal(t, "po", v.Active) @@ -22,7 +22,7 @@ func TestViewValidate(t *testing.T) { } func TestViewValidateBlank(t *testing.T) { - var v config.View + var v data.View v.Validate() assert.Equal(t, "po", v.Active) } diff --git a/internal/config/feature.go b/internal/config/feature.go index f94f5855a4..4b1fd75bcd 100644 --- a/internal/config/feature.go +++ b/internal/config/feature.go @@ -3,12 +3,12 @@ package config -// FeatureGates represents K9s opt-in features. -type FeatureGates struct { - NodeShell bool `yaml:"nodeShell"` -} +// // FeatureGates represents K9s opt-in features. +// type FeatureGates struct { +// NodeShell bool `yaml:"nodeShell"` +// } -// NewFeatureGates returns a new feature gate. -func NewFeatureGates() *FeatureGates { - return &FeatureGates{} -} +// // NewFeatureGates returns a new feature gate. +// func NewFeatureGates() *FeatureGates { +// return &FeatureGates{} +// } diff --git a/internal/config/files.go b/internal/config/files.go new file mode 100644 index 0000000000..05c40e2796 --- /dev/null +++ b/internal/config/files.go @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config + +import ( + _ "embed" + "os" + "os/user" + "path/filepath" + "regexp" + + "github.com/derailed/k9s/internal/config/data" + + "github.com/adrg/xdg" + "github.com/rs/zerolog/log" +) + +const ( + // K9sConfigDir represents k9s configuration dir env var. + K9sConfigDir = "K9S_CONFIG_DIR" + + // AppName tracks k9s app name. + AppName = "k9s" + + K9sLogsFile = "k9s.log" +) + +var ( + //go:embed templates/benchmarks.yaml + // benchmarkTpl tracks benchmark default config template + benchmarkTpl []byte + + //go:embed templates/aliases.yaml + // aliasesTpl tracks aliases default config template + aliasesTpl []byte + + //go:embed templates/hotkeys.yaml + // hotkeysTpl tracks hotkeys default config template + hotkeysTpl []byte + + //go:embed templates/stock-skin.yaml + // stockSkinTpl tracks stock skin template + stockSkinTpl []byte +) + +var ( + // AppConfigDir tracks main k9s config home directory. + AppConfigDir string + + // AppSkinsDir tracks skins data directory. + AppSkinsDir string + + // AppBenchmarksDir tracks benchmarks results directory. + AppBenchmarksDir string + + // AppDumpsDir tracks screen dumps data directory. + AppDumpsDir string + + // AppContextsDir tracks contexts data directory. + AppContextsDir string + + // AppConfigFile tracks k9s config file. + AppConfigFile string + + // AppLogFile tracks k9s logs file. + AppLogFile string + + // AppViewsFile tracks custom views config file. + AppViewsFile string + + // AppAliasesFile tracks aliases config file. + AppAliasesFile string + + // AppPluginsFile tracks plugins config file. + AppPluginsFile string + + // AppHotKeysFile tracks hotkeys config file. + AppHotKeysFile string +) + +// InitLogsLoc initializes K9s logs location. +func InitLogLoc() error { + if hasK9sConfigEnv() { + tmpDir, err := userTmpDir() + if err != nil { + return err + } + AppLogFile = filepath.Join(tmpDir, K9sLogsFile) + return nil + } + + var err error + AppLogFile, err = xdg.StateFile(filepath.Join(AppName, K9sLogsFile)) + + return err +} + +// InitLocs initializes k9s artifacts locations. +func InitLocs() error { + if hasK9sConfigEnv() { + return initK9sEnvLocs() + } + + return initXDGLocs() +} + +func initK9sEnvLocs() error { + AppConfigDir = os.Getenv(K9sConfigDir) + if err := data.EnsureFullPath(AppConfigDir, data.DefaultDirMod); err != nil { + return err + } + + AppDumpsDir = filepath.Join(AppConfigDir, "screen-dumps") + if err := data.EnsureFullPath(AppDumpsDir, data.DefaultDirMod); err != nil { + log.Warn().Err(err).Msgf("Unable to create screen-dumps dir: %s", AppDumpsDir) + } + AppBenchmarksDir = filepath.Join(AppConfigDir, "benchmarks") + if err := data.EnsureFullPath(AppBenchmarksDir, data.DefaultDirMod); err != nil { + log.Warn().Err(err).Msgf("Unable to create benchmarks dir: %s", AppBenchmarksDir) + } + AppSkinsDir = filepath.Join(AppConfigDir, "skins") + if err := data.EnsureFullPath(AppSkinsDir, data.DefaultDirMod); err != nil { + log.Warn().Err(err).Msgf("Unable to create skins dir: %s", AppSkinsDir) + } + AppContextsDir = filepath.Join(AppConfigDir, "clusters") + if err := data.EnsureFullPath(AppContextsDir, data.DefaultDirMod); err != nil { + log.Warn().Err(err).Msgf("Unable to create clusters dir: %s", AppContextsDir) + } + + AppConfigFile = filepath.Join(AppConfigDir, data.MainConfigFile) + AppHotKeysFile = filepath.Join(AppConfigDir, "hotkeys.yaml") + AppAliasesFile = filepath.Join(AppConfigDir, "aliases.yaml") + AppPluginsFile = filepath.Join(AppConfigDir, "plugins.yaml") + AppViewsFile = filepath.Join(AppConfigDir, "views.yaml") + + return nil +} + +func initXDGLocs() error { + var err error + + AppConfigDir, err = xdg.ConfigFile(AppName) + if err != nil { + return err + } + + AppConfigFile, err = xdg.ConfigFile(filepath.Join(AppName, data.MainConfigFile)) + if err != nil { + return err + } + + AppHotKeysFile = filepath.Join(AppConfigDir, "hotkeys.yaml") + AppAliasesFile = filepath.Join(AppConfigDir, "aliases.yaml") + AppPluginsFile = filepath.Join(AppConfigDir, "plugins.yaml") + AppViewsFile = filepath.Join(AppConfigDir, "views.yaml") + + AppSkinsDir = filepath.Join(AppConfigDir, "skins") + if err := data.EnsureFullPath(AppSkinsDir, data.DefaultDirMod); err != nil { + log.Warn().Err(err).Msgf("No skins dir detected") + } + + AppDumpsDir, err = xdg.StateFile(filepath.Join(AppName, "screen-dumps")) + if err != nil { + return err + } + + AppBenchmarksDir, err = xdg.StateFile(filepath.Join(AppName, "benchmarks")) + if err != nil { + log.Warn().Err(err).Msgf("No benchmarks dir detected") + } + + dataDir, err := xdg.DataFile(AppName) + if err != nil { + return err + } + AppContextsDir = filepath.Join(dataDir, "clusters") + if err := data.EnsureFullPath(AppContextsDir, data.DefaultDirMod); err != nil { + log.Warn().Err(err).Msgf("No context dir detected") + } + + return nil +} + +var invalidPathCharsRX = regexp.MustCompile(`[:/]+`) + +// SanitizeFileName ensure file spec is valid. +func SanitizeFileName(name string) string { + return invalidPathCharsRX.ReplaceAllString(name, "-") +} + +// AppContextDir generates a valid context config dir. +func AppContextDir(cluster, context string) string { + return filepath.Join(AppContextsDir, sanContextSubpath(cluster, context)) +} + +// AppContextAliasesFile generates a valid context specific aliases file path. +func AppContextAliasesFile(cluster, context string) string { + return filepath.Join(AppContextsDir, sanContextSubpath(cluster, context), "aliases.yaml") +} + +// AppContextPluginsFile generates a valid context specific plugins file path. +func AppContextPluginsFile(cluster, context string) string { + return filepath.Join(AppContextsDir, sanContextSubpath(cluster, context), "plugins.yaml") +} + +// AppContextHotkeysFile generates a valid context specific hotkeys file path. +func AppContextHotkeysFile(cluster, context string) string { + return filepath.Join(AppContextsDir, sanContextSubpath(cluster, context), "hotkeys.yaml") +} + +// AppContextConfig generates a valid context config file path. +func AppContextConfig(cluster, context string) string { + return filepath.Join(AppContextDir(cluster, context), data.MainConfigFile) +} + +// DumpsDir generates a valid context dump directory. +func DumpsDir(cluster, context string) (string, error) { + dir := filepath.Join(AppDumpsDir, sanContextSubpath(cluster, context)) + + return dir, data.EnsureDirPath(dir, data.DefaultDirMod) +} + +// EnsureBenchmarksDir generates a valid benchmark results directory. +func EnsureBenchmarksDir(cluster, context string) (string, error) { + dir := filepath.Join(AppBenchmarksDir, sanContextSubpath(cluster, context)) + + return dir, data.EnsureDirPath(dir, data.DefaultDirMod) +} + +// EnsureBenchmarksCfgFile generates a valid benchmark file. +func EnsureBenchmarksCfgFile(cluster, context string) (string, error) { + f := filepath.Join(AppContextDir(cluster, context), "benchmarks.yaml") + if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil { + return "", err + } + if _, err := os.Stat(f); os.IsNotExist(err) { + return f, os.WriteFile(f, benchmarkTpl, data.DefaultFileMod) + } + + return f, nil +} + +// EnsureAliasesCfgFile generates a valid aliases file. +func EnsureAliasesCfgFile() (string, error) { + f := filepath.Join(AppConfigDir, "aliases.yaml") + if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil { + return "", err + } + if _, err := os.Stat(f); os.IsNotExist(err) { + return f, os.WriteFile(f, aliasesTpl, data.DefaultFileMod) + } + + return f, nil +} + +// EnsureHotkeysCfgFile generates a valid hotkeys file. +func EnsureHotkeysCfgFile() (string, error) { + f := filepath.Join(AppConfigDir, "hotkeys.yaml") + if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil { + return "", err + } + if _, err := os.Stat(f); os.IsNotExist(err) { + return f, os.WriteFile(f, hotkeysTpl, data.DefaultFileMod) + } + + return f, nil +} + +// SkinFileFromName generate skin file path from spec. +func SkinFileFromName(n string) string { + return filepath.Join(AppSkinsDir, n+".yaml") +} + +// Helpers... + +func sanContextSubpath(cluster, context string) string { + return filepath.Join(SanitizeFileName(cluster), SanitizeFileName(context)) +} + +func hasK9sConfigEnv() bool { + return os.Getenv(K9sConfigDir) != "" +} + +func userTmpDir() (string, error) { + u, err := user.Current() + if err != nil { + return "", err + } + + dir := filepath.Join(os.TempDir(), AppName, u.Username) + if err := data.EnsureFullPath(dir, data.DefaultDirMod); err != nil { + return "", err + } + + return dir, nil +} diff --git a/internal/config/files_test.go b/internal/config/files_test.go new file mode 100644 index 0000000000..ca204e09ee --- /dev/null +++ b/internal/config/files_test.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config_test + +import ( + "os" + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" + "github.com/stretchr/testify/assert" +) + +func TestEnsureBenchmarkCfg(t *testing.T) { + os.Setenv(config.K9sConfigDir, "/tmp/test-config") + assert.NoError(t, config.InitLocs()) + defer assert.NoError(t, os.RemoveAll(config.K9sConfigDir)) + + 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)) + + uu := map[string]struct { + cluster, context string + f, e string + }{ + "not-exist": { + cluster: "cl-1", + context: "ct-1", + f: "/tmp/test-config/clusters/cl-1/ct-1/benchmarks.yaml", + e: "benchmarks:\n defaults:\n concurrency: 2\n requests: 200", + }, + "exist": { + cluster: "cl-1", + context: "ct-2", + f: "/tmp/test-config/clusters/cl-1/ct-2/benchmarks.yaml", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + f, err := config.EnsureBenchmarksCfgFile(u.cluster, u.context) + assert.NoError(t, err) + assert.Equal(t, u.f, f) + bb, err := os.ReadFile(f) + assert.NoError(t, err) + assert.Equal(t, u.e, string(bb)) + }) + } +} diff --git a/internal/config/flags.go b/internal/config/flags.go index aa2939ea47..8e7a78db45 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -3,12 +3,6 @@ package config -import ( - "fmt" - "os" - "path/filepath" -) - const ( // DefaultRefreshRate represents the refresh interval. DefaultRefreshRate = 2 // secs @@ -21,7 +15,7 @@ const ( ) // DefaultLogFile represents the default K9s log file. -var DefaultLogFile = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-%s.log", MustK9sUser())) +// var DefaultLogFile = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-%s.log", MustK9sUser())) // Flags represents K9s configuration flags. type Flags struct { @@ -43,7 +37,7 @@ func NewFlags() *Flags { return &Flags{ RefreshRate: intPtr(DefaultRefreshRate), LogLevel: strPtr(DefaultLogLevel), - LogFile: strPtr(DefaultLogFile), + LogFile: strPtr(AppLogFile), Headless: boolPtr(false), Logoless: boolPtr(false), Command: strPtr(DefaultCommand), @@ -51,7 +45,7 @@ func NewFlags() *Flags { ReadOnly: boolPtr(false), Write: boolPtr(false), Crumbsless: boolPtr(false), - ScreenDumpDir: strPtr(K9sDefaultScreenDumpDir), + ScreenDumpDir: strPtr(AppDumpsDir), } } diff --git a/internal/config/helpers.go b/internal/config/helpers.go index bc14a34898..4d2d239784 100644 --- a/internal/config/helpers.go +++ b/internal/config/helpers.go @@ -4,39 +4,18 @@ package config import ( - "os" "os/user" - "path/filepath" - "regexp" + "github.com/derailed/k9s/internal/config/data" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" ) -const ( - // DefaultDirMod default unix perms for k9s directory. - DefaultDirMod os.FileMode = 0755 - // DefaultFileMod default unix perms for k9s files. - DefaultFileMod os.FileMode = 0600 -) - -var invalidPathCharsRX = regexp.MustCompile(`[:/]+`) - // SanitizeFilename sanitizes the dump filename. func SanitizeFilename(name string) string { return invalidPathCharsRX.ReplaceAllString(name, "-") } -// InList check if string is in a collection of strings. -func InList(ll []string, n string) bool { - for _, l := range ll { - if l == n { - return true - } - } - return false -} - // InNSList check if ns is in an ns collection. func InNSList(nn []interface{}, ns string) bool { ss := make([]string, len(nn)) @@ -45,7 +24,7 @@ func InNSList(nn []interface{}, ns string) bool { ss[i] = nsp.Name } } - return InList(ss, ns) + return data.InList(ss, ns) } // MustK9sUser establishes current user identity or fail. @@ -57,22 +36,6 @@ func MustK9sUser() string { return usr.Username } -// EnsureDirPath ensures a directory exist from the given path. -func EnsureDirPath(path string, mod os.FileMode) error { - return EnsureFullPath(filepath.Dir(path), mod) -} - -// EnsureFullPath ensures a directory exist from the given path. -func EnsureFullPath(path string, mod os.FileMode) error { - if _, err := os.Stat(path); os.IsNotExist(err) { - if err = os.MkdirAll(path, mod); err != nil { - return err - } - } - - return nil -} - // IsBoolSet checks if a bool prt is set. func IsBoolSet(b *bool) bool { return b != nil && *b diff --git a/internal/config/helpers_test.go b/internal/config/helpers_test.go index 06dcf9442f..2c29e2d8d1 100644 --- a/internal/config/helpers_test.go +++ b/internal/config/helpers_test.go @@ -4,8 +4,6 @@ package config_test import ( - "os" - "path/filepath" "testing" "github.com/derailed/k9s/internal/config" @@ -14,24 +12,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestHelperInList(t *testing.T) { - uu := []struct { - item string - list []string - expected bool - }{ - {"a", []string{}, false}, - {"", []string{}, false}, - {"", []string{""}, true}, - {"a", []string{"a", "b", "c", "d"}, true}, - {"z", []string{"a", "b", "c", "d"}, false}, - } - - for _, u := range uu { - assert.Equal(t, u.expected, config.InList(u.list, u.item)) - } -} - func TestHelperInNSList(t *testing.T) { uu := []struct { item string @@ -58,30 +38,3 @@ func TestHelperInNSList(t *testing.T) { assert.Equal(t, u.expected, config.InNSList(u.list, u.item)) } } - -func TestEnsureDirPathNone(t *testing.T) { - var mod os.FileMode = 0744 - dir := filepath.Join("/tmp", "fred") - os.Remove(dir) - - path := filepath.Join(dir, "duh.yml") - assert.NoError(t, config.EnsureDirPath(path, mod)) - - p, err := os.Stat(dir) - assert.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", "blee") - os.Remove(dir) - assert.NoError(t, os.Mkdir(dir, mod)) - - path := filepath.Join(dir, "duh.yml") - assert.NoError(t, config.EnsureDirPath(path, mod)) - - p, err := os.Stat(dir) - assert.NoError(t, err) - assert.Equal(t, "drwxr--r--", p.Mode().String()) -} diff --git a/internal/config/hotkey.go b/internal/config/hotkey.go index ef3041de59..fa292a98f9 100644 --- a/internal/config/hotkey.go +++ b/internal/config/hotkey.go @@ -5,17 +5,13 @@ package config import ( "os" - "path/filepath" "gopkg.in/yaml.v2" ) -// K9sHotKeys manages K9s hotKeys. -var K9sHotKeys = YamlExtension(filepath.Join(K9sHome(), "hotkey.yml")) - // HotKeys represents a collection of plugins. type HotKeys struct { - HotKey map[string]HotKey `yaml:"hotKey"` + HotKey map[string]HotKey `yaml:"hotKeys"` } // HotKey describes a K9s hotkey. @@ -34,7 +30,7 @@ func NewHotKeys() HotKeys { // Load K9s plugins. func (h HotKeys) Load() error { - return h.LoadHotKeys(K9sHotKeys) + return h.LoadHotKeys(AppHotKeysFile) } // LoadHotKeys loads plugins from a given file. diff --git a/internal/config/hotkey_test.go b/internal/config/hotkey_test.go index 91fe6a4b5a..80b244c735 100644 --- a/internal/config/hotkey_test.go +++ b/internal/config/hotkey_test.go @@ -12,7 +12,7 @@ import ( func TestHotKeyLoad(t *testing.T) { h := config.NewHotKeys() - assert.Nil(t, h.LoadHotKeys("testdata/hot_key.yml")) + assert.Nil(t, h.LoadHotKeys("testdata/hotkeys.yaml")) assert.Equal(t, 1, len(h.HotKey)) diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 8dbaacb638..2fae52b9d1 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -4,37 +4,28 @@ package config import ( - "github.com/derailed/k9s/internal/client" -) + "errors" + "path/filepath" -const ( - defaultRefreshRate = 2 - defaultMaxConnRetry = 5 + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config/data" ) // K9s tracks K9s configuration options. type K9s struct { - LiveViewAutoRefresh bool `yaml:"liveViewAutoRefresh"` - RefreshRate int `yaml:"refreshRate"` - MaxConnRetry int `yaml:"maxConnRetry"` - EnableMouse bool `yaml:"enableMouse"` - EnableImageScan bool `yaml:"enableImageScan"` - Headless bool `yaml:"headless"` - Logoless bool `yaml:"logoless"` - Crumbsless bool `yaml:"crumbsless"` - ReadOnly bool `yaml:"readOnly"` - NoExitOnCtrlC bool `yaml:"noExitOnCtrlC"` - NoIcons bool `yaml:"noIcons"` - ShellPod *ShellPod `yaml:"shellPod"` - SkipLatestRevCheck bool `yaml:"skipLatestRevCheck"` - Logger *Logger `yaml:"logger"` - CurrentContext string `yaml:"currentContext"` - CurrentCluster string `yaml:"currentCluster"` - KeepMissingClusters bool `yaml:"keepMissingClusters"` - Clusters map[string]*Cluster `yaml:"clusters,omitempty"` - Thresholds Threshold `yaml:"thresholds"` - ScreenDumpDir string `yaml:"screenDumpDir"` - DisablePodCounting bool `yaml:"disablePodCounting"` + LiveViewAutoRefresh bool `yaml:"liveViewAutoRefresh"` + ScreenDumpDir string `yaml:"screenDumpDir,omitempty"` + RefreshRate int `yaml:"refreshRate"` + MaxConnRetry int `yaml:"maxConnRetry"` + ReadOnly bool `yaml:"readOnly"` + NoExitOnCtrlC bool `yaml:"noExitOnCtrlC"` + UI UI `yaml:"ui"` + SkipLatestRevCheck bool `yaml:"skipLatestRevCheck"` + DisablePodCounting bool `yaml:"disablePodCounting"` + ShellPod *ShellPod `yaml:"shellPod"` + ImageScans *ImageScans `yaml:"imageScans"` + Logger *Logger `yaml:"logger"` + Thresholds Threshold `yaml:"thresholds"` manualRefreshRate int manualHeadless *bool manualLogoless *bool @@ -42,36 +33,151 @@ type K9s struct { manualReadOnly *bool manualCommand *string manualScreenDumpDir *string + dir *data.Dir + activeContextName string + activeConfig *data.Config + conn client.Connection + ks data.KubeSettings } // NewK9s create a new K9s configuration. -func NewK9s() *K9s { +func NewK9s(conn client.Connection, ks data.KubeSettings) *K9s { return &K9s{ RefreshRate: defaultRefreshRate, MaxConnRetry: defaultMaxConnRetry, + ScreenDumpDir: AppDumpsDir, Logger: NewLogger(), - Clusters: make(map[string]*Cluster), Thresholds: NewThreshold(), - ScreenDumpDir: K9sDefaultScreenDumpDir, ShellPod: NewShellPod(), + ImageScans: NewImageScans(), + dir: data.NewDir(AppContextsDir, conn, ks), + conn: conn, + ks: ks, + } +} + +func (k *K9s) Save() error { + if k.activeConfig != nil { + path := filepath.Join( + AppContextsDir, + k.activeConfig.Context.ClusterName, + k.activeContextName, + data.MainConfigFile, + ) + return k.activeConfig.Save(path) + } + + return nil +} + +func (k *K9s) Refine(k1 *K9s) { + k.LiveViewAutoRefresh = k1.LiveViewAutoRefresh + k.ScreenDumpDir = k1.ScreenDumpDir + k.RefreshRate = k1.RefreshRate + k.MaxConnRetry = k1.MaxConnRetry + k.ReadOnly = k1.ReadOnly + k.NoExitOnCtrlC = k1.NoExitOnCtrlC + k.UI = k1.UI + k.SkipLatestRevCheck = k1.SkipLatestRevCheck + k.DisablePodCounting = k1.DisablePodCounting + k.ShellPod = k1.ShellPod + k.ImageScans = k1.ImageScans + k.Logger = k1.Logger + k.Thresholds = k1.Thresholds +} + +func (k *K9s) Generate(k9sFlags *Flags) { + if *k9sFlags.RefreshRate != DefaultRefreshRate { + k.OverrideRefreshRate(*k9sFlags.RefreshRate) } + + k.OverrideHeadless(*k9sFlags.Headless) + k.OverrideLogoless(*k9sFlags.Logoless) + k.OverrideCrumbsless(*k9sFlags.Crumbsless) + k.OverrideReadOnly(*k9sFlags.ReadOnly) + k.OverrideWrite(*k9sFlags.Write) + k.OverrideCommand(*k9sFlags.Command) + k.OverrideScreenDumpDir(*k9sFlags.ScreenDumpDir) } -func (k *K9s) CurrentContextDir() string { - return SanitizeFilename(k.CurrentContext) +// OverrideScreenDumpDir set the screen dump dir manually. +func (k *K9s) OverrideScreenDumpDir(dir string) { + k.manualScreenDumpDir = &dir } -// ActivateCluster initializes the active cluster is not present. -func (k *K9s) ActivateCluster(ns string) { - if k.Clusters == nil { - k.Clusters = map[string]*Cluster{} +func (k *K9s) GetScreenDumpDir() string { + screenDumpDir := k.ScreenDumpDir + if k.manualScreenDumpDir != nil && *k.manualScreenDumpDir != "" { + screenDumpDir = *k.manualScreenDumpDir + } + if screenDumpDir == "" { + screenDumpDir = AppDumpsDir + } + + return screenDumpDir +} + +func (k *K9s) Reset() { + k.activeConfig, k.activeContextName = nil, "" +} + +func (k *K9s) ActiveContextDir() string { + if k.activeConfig == nil { + return "na" + } + + return filepath.Join( + SanitizeFileName(k.activeConfig.Context.ClusterName), + SanitizeFileName(k.ActiveContextName()), + ) +} + +func (k *K9s) ActiveContextNamespace() (string, error) { + if k.activeConfig != nil { + return k.activeConfig.Context.Namespace.Active, nil + } + + return "", errors.New("context config is not set") +} + +func (k *K9s) ActiveContextName() string { + return k.activeContextName +} + +// ActiveContext returns the currently active context. +func (k *K9s) ActiveContext() (*data.Context, error) { + if k.activeConfig != nil { + return k.activeConfig.Context, nil + } + + ct, err := k.ActivateContext(k.activeContextName) + if err != nil { + return nil, err + } + + return ct, nil +} + +// ActivateContext initializes the active context is not present. +func (k *K9s) ActivateContext(n string) (*data.Context, error) { + k.activeContextName = n + ct, err := k.ks.GetContext(k.activeContextName) + if err != nil { + return nil, err } - if _, ok := k.Clusters[k.CurrentCluster]; ok { - return + cfg, err := k.dir.Load(n, ct) + if err != nil { + return nil, err + } + k.activeConfig = cfg + // If the context specifies a default namespace, use it! + if k.conn != nil { + if ns := k.conn.ActiveNamespace(); ns != client.BlankNamespace { + k.activeConfig.Context.Namespace.Active = ns + } } - cl := NewCluster() - cl.Namespace.Active = ns - k.Clusters[k.CurrentCluster] = cl + + return cfg.Context, nil } // OverrideRefreshRate set the refresh rate manually. @@ -114,14 +220,9 @@ func (k *K9s) OverrideCommand(cmd string) { k.manualCommand = &cmd } -// OverrideScreenDumpDir set the screen dump dir manually. -func (k *K9s) OverrideScreenDumpDir(dir string) { - k.manualScreenDumpDir = &dir -} - // IsHeadless returns headless setting. func (k *K9s) IsHeadless() bool { - h := k.Headless + h := k.UI.Headless if k.manualHeadless != nil && *k.manualHeadless { h = *k.manualHeadless } @@ -131,7 +232,7 @@ func (k *K9s) IsHeadless() bool { // IsLogoless returns logoless setting. func (k *K9s) IsLogoless() bool { - h := k.Logoless + h := k.UI.Logoless if k.manualLogoless != nil && *k.manualLogoless { h = *k.manualLogoless } @@ -141,7 +242,7 @@ func (k *K9s) IsLogoless() bool { // IsCrumbsless returns crumbsless setting. func (k *K9s) IsCrumbsless() bool { - h := k.Crumbsless + h := k.UI.Crumbsless if k.manualCrumbsless != nil && *k.manualCrumbsless { h = *k.manualCrumbsless } @@ -165,34 +266,11 @@ func (k *K9s) IsReadOnly() bool { if k.manualReadOnly != nil { readOnly = *k.manualReadOnly } - - return readOnly -} - -// ActiveCluster returns the currently active cluster. -func (k *K9s) ActiveCluster() *Cluster { - if k.Clusters == nil { - k.Clusters = map[string]*Cluster{} - } - if c, ok := k.Clusters[k.CurrentCluster]; ok { - return c - } - k.Clusters[k.CurrentCluster] = NewCluster() - - return k.Clusters[k.CurrentCluster] -} - -func (k *K9s) GetScreenDumpDir() string { - screenDumpDir := k.ScreenDumpDir - if k.manualScreenDumpDir != nil && *k.manualScreenDumpDir != "" { - screenDumpDir = *k.manualScreenDumpDir + if k.activeConfig != nil && k.activeConfig.Context.ReadOnly { + readOnly = true } - if screenDumpDir == "" { - return K9sDefaultScreenDumpDir - } - - return screenDumpDir + return readOnly } func (k *K9s) validateDefaults() { @@ -202,44 +280,19 @@ func (k *K9s) validateDefaults() { if k.MaxConnRetry <= 0 { k.MaxConnRetry = defaultMaxConnRetry } - if k.ScreenDumpDir == "" { - k.ScreenDumpDir = K9sDefaultScreenDumpDir - } -} - -func (k *K9s) validateClusters(c client.Connection, ks KubeSettings) { - cc, err := ks.ClusterNames() - if err != nil { - return - } - for key, cluster := range k.Clusters { - cluster.Validate(c, ks) - // if the cluster is defined in the $KUBECONFIG file, keep it in the k9s config file - if _, ok := cc[key]; ok { - continue - } - - // if we asked to keep the clusters in the config file - if k.KeepMissingClusters { - continue - } - - // else remove it from the k9s config file - if k.CurrentCluster == key { - k.CurrentCluster = "" - } - delete(k.Clusters, key) - } } // Validate the current configuration. -func (k *K9s) Validate(c client.Connection, ks KubeSettings) { +func (k *K9s) Validate(c client.Connection, ks data.KubeSettings) { k.validateDefaults() - if k.Clusters == nil { - k.Clusters = map[string]*Cluster{} + if k.activeConfig == nil { + if n, err := ks.CurrentContextName(); err == nil { + _, _ = k.ActivateContext(n) + } + } + if k.ImageScans == nil { + k.ImageScans = NewImageScans() } - k.validateClusters(c, ks) - if k.ShellPod == nil { k.ShellPod = NewShellPod() } @@ -254,18 +307,4 @@ func (k *K9s) Validate(c client.Connection, ks KubeSettings) { k.Thresholds = NewThreshold() } k.Thresholds.Validate(c, ks) - - if context, err := ks.CurrentContextName(); err == nil && len(k.CurrentContext) == 0 { - k.CurrentContext = context - k.CurrentCluster = "" - } - - if cl, err := ks.CurrentClusterName(); err == nil && len(k.CurrentCluster) == 0 { - k.CurrentCluster = cl - } - - if _, ok := k.Clusters[k.CurrentCluster]; !ok { - k.Clusters[k.CurrentCluster] = NewCluster() - } - k.Clusters[k.CurrentCluster].Validate(c, ks) } diff --git a/internal/config/k9s_test.go b/internal/config/k9s_test.go index b4c233ada8..67c1c461b3 100644 --- a/internal/config/k9s_test.go +++ b/internal/config/k9s_test.go @@ -7,165 +7,37 @@ import ( "testing" "github.com/derailed/k9s/internal/config" - m "github.com/petergtz/pegomock" + "github.com/derailed/k9s/internal/config/mock" "github.com/stretchr/testify/assert" ) -func TestIsReadOnly(t *testing.T) { - uu := map[string]struct { - config string - read, write bool - readOnly bool - }{ - "writable": { - config: "k9s.yml", - }, - "writable_read_override": { - config: "k9s.yml", - read: true, - readOnly: true, - }, - "writable_write_override": { - config: "k9s.yml", - write: true, - }, - "readonly": { - config: "k9s_readonly.yml", - readOnly: true, - }, - "readonly_read_override": { - config: "k9s_readonly.yml", - read: true, - readOnly: true, - }, - "readonly_write_override": { - config: "k9s_readonly.yml", - write: true, - }, - "readonly_both_override": { - config: "k9s_readonly.yml", - read: true, - write: true, - }, - } - - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Nil(t, cfg.Load("testdata/"+u.config)) - cfg.K9s.OverrideReadOnly(u.read) - cfg.K9s.OverrideWrite(u.write) - assert.Equal(t, u.readOnly, cfg.K9s.IsReadOnly()) - }) - } -} - -func TestK9sValidate(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - - mk := NewMockKubeSettings() - m.When(mk.CurrentContextName()).ThenReturn("ctx1", nil) - m.When(mk.CurrentClusterName()).ThenReturn("c1", nil) - m.When(mk.ClusterNames()).ThenReturn(map[string]struct{}{"c1": {}, "c2": {}}, nil) - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"}) - - c := config.NewK9s() - c.Validate(mc, mk) - - assert.Equal(t, 2, c.RefreshRate) - assert.Equal(t, int64(100), c.Logger.TailCount) - assert.Equal(t, 5000, c.Logger.BufferSize) - assert.Equal(t, "ctx1", c.CurrentContext) - assert.Equal(t, "c1", c.CurrentCluster) - assert.Equal(t, 1, len(c.Clusters)) - assert.Equal(t, config.K9sDefaultScreenDumpDir, c.GetScreenDumpDir()) - _, ok := c.Clusters[c.CurrentCluster] - assert.True(t, ok) -} - -func TestK9sValidateBlank(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - - mk := NewMockKubeSettings() - m.When(mk.CurrentContextName()).ThenReturn("ctx1", nil) - m.When(mk.CurrentClusterName()).ThenReturn("c1", nil) - m.When(mk.ClusterNames()).ThenReturn(map[string]struct{}{"c1": {}, "c2": {}}, nil) - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"}) - - var c config.K9s - c.Validate(mc, mk) - - assert.Equal(t, 2, c.RefreshRate) - assert.Equal(t, int64(100), c.Logger.TailCount) - assert.Equal(t, 5000, c.Logger.BufferSize) - assert.Equal(t, "ctx1", c.CurrentContext) - assert.Equal(t, "c1", c.CurrentCluster) - assert.Equal(t, 1, len(c.Clusters)) - _, ok := c.Clusters[c.CurrentCluster] - assert.True(t, ok) -} - -func TestK9sActiveClusterZero(t *testing.T) { - c := config.NewK9s() - c.CurrentCluster = "fred" - cl := c.ActiveCluster() - assert.NotNil(t, cl) - assert.Equal(t, "default", cl.Namespace.Active) - assert.Equal(t, 1, len(cl.Namespace.Favorites)) -} - -func TestK9sActiveClusterBlank(t *testing.T) { - var c config.K9s - cl := c.ActiveCluster() - assert.Equal(t, config.NewCluster(), cl) -} - -func TestK9sActiveCluster(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - - cl := cfg.K9s.ActiveCluster() - assert.NotNil(t, cl) - assert.Equal(t, "kube-system", cl.Namespace.Active) - assert.Equal(t, 5, len(cl.Namespace.Favorites)) -} - func TestGetScreenDumpDir(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) + cfg := mock.NewMockConfig() + assert.Nil(t, cfg.Load("testdata/k9s.yaml")) assert.Equal(t, "/tmp", cfg.K9s.GetScreenDumpDir()) } func TestGetScreenDumpDirOverride(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - cfg.K9s.OverrideScreenDumpDir("/override") + cfg := mock.NewMockConfig() + assert.Nil(t, cfg.Load("testdata/k9s.yaml")) + cfg.K9s.OverrideScreenDumpDir("/override") assert.Equal(t, "/override", cfg.K9s.GetScreenDumpDir()) } func TestGetScreenDumpDirOverrideEmpty(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - cfg.K9s.OverrideScreenDumpDir("") + cfg := mock.NewMockConfig() + assert.Nil(t, cfg.Load("testdata/k9s.yaml")) + cfg.K9s.OverrideScreenDumpDir("") assert.Equal(t, "/tmp", cfg.K9s.GetScreenDumpDir()) } func TestGetScreenDumpDirEmpty(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s1.yml")) - cfg.K9s.OverrideScreenDumpDir("") + cfg := mock.NewMockConfig() - assert.Equal(t, config.K9sDefaultScreenDumpDir, cfg.K9s.GetScreenDumpDir()) + assert.Nil(t, cfg.Load("testdata/k9s1.yaml")) + cfg.K9s.OverrideScreenDumpDir("") + assert.Equal(t, config.AppDumpsDir, cfg.K9s.GetScreenDumpDir()) } diff --git a/internal/config/logger.go b/internal/config/logger.go index 13cb96b108..4cca7e9736 100644 --- a/internal/config/logger.go +++ b/internal/config/logger.go @@ -5,6 +5,7 @@ package config import ( "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config/data" ) const ( @@ -38,7 +39,7 @@ func NewLogger() *Logger { } // Validate checks thresholds and make sure we're cool. If not use defaults. -func (l *Logger) Validate(_ client.Connection, _ KubeSettings) { +func (l *Logger) Validate(_ client.Connection, _ data.KubeSettings) { if l.TailCount <= 0 { l.TailCount = DefaultLoggerTailCount } diff --git a/internal/config/mock/test_helpers.go b/internal/config/mock/test_helpers.go new file mode 100644 index 0000000000..b5378bf2d5 --- /dev/null +++ b/internal/config/mock/test_helpers.go @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package mock + +import ( + "fmt" + "os" + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + version "k8s.io/apimachinery/pkg/version" + "k8s.io/cli-runtime/pkg/genericclioptions" + disk "k8s.io/client-go/discovery/cached/disk" + dynamic "k8s.io/client-go/dynamic" + kubernetes "k8s.io/client-go/kubernetes" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd/api" + versioned "k8s.io/metrics/pkg/client/clientset/versioned" +) + +func EnsureDir(d string) error { + if _, err := os.Stat(d); os.IsNotExist(err) { + return os.MkdirAll(d, 0700) + } + if err := os.RemoveAll(d); err != nil { + return err + } + + return os.MkdirAll(d, 0700) +} + +func NewMockConfig() *config.Config { + config.AppContextsDir = "/tmp/test" + cl, ct := "cl-1", "ct-1" + flags := genericclioptions.ConfigFlags{ + ClusterName: &cl, + Context: &ct, + } + cfg := config.NewConfig( + NewMockKubeSettings(&flags), + ) + + return cfg +} + +type mockKubeSettings struct { + flags *genericclioptions.ConfigFlags + cts map[string]*api.Context +} + +func NewMockKubeSettings(f *genericclioptions.ConfigFlags) mockKubeSettings { + _, idx, _ := strings.Cut(*f.ClusterName, "-") + ctId := "ct-" + idx + + return mockKubeSettings{ + flags: f, + cts: map[string]*api.Context{ + ctId + "-1": { + Cluster: *f.ClusterName, + Namespace: "", + }, + ctId + "-2": { + Cluster: *f.ClusterName, + Namespace: "ns-1", + }, + ctId + "-3": { + Cluster: *f.ClusterName, + Namespace: client.DefaultNamespace, + }, + }, + } +} +func (m mockKubeSettings) CurrentContextName() (string, error) { + return *m.flags.Context, nil +} +func (m mockKubeSettings) CurrentClusterName() (string, error) { + return *m.flags.ClusterName, nil +} +func (m mockKubeSettings) CurrentNamespaceName() (string, error) { + return "default", nil +} +func (m mockKubeSettings) GetContext(s string) (*api.Context, error) { + ct, ok := m.cts[s] + if !ok { + return nil, fmt.Errorf("no context found for: %q", s) + } + return ct, nil +} +func (m mockKubeSettings) CurrentContext() (*api.Context, error) { + return m.GetContext(*m.flags.Context) +} +func (m mockKubeSettings) ContextNames() (map[string]struct{}, error) { + mm := make(map[string]struct{}, len(m.cts)) + for k := range m.cts { + mm[k] = struct{}{} + } + + return mm, nil +} + +type mockConnection struct{} + +func NewMockConnection() mockConnection { + return mockConnection{} +} +func (m mockConnection) CanI(ns, gvr string, verbs []string) (bool, error) { + return true, nil +} +func (m mockConnection) Config() *client.Config { + return nil +} +func (m mockConnection) ConnectionOK() bool { + return false +} +func (m mockConnection) Dial() (kubernetes.Interface, error) { + return nil, nil +} +func (m mockConnection) DialLogs() (kubernetes.Interface, error) { + return nil, nil +} +func (m mockConnection) SwitchContext(ctx string) error { + return nil +} +func (m mockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { + return nil, nil +} +func (m mockConnection) RestConfig() (*restclient.Config, error) { + return nil, nil +} +func (m mockConnection) MXDial() (*versioned.Clientset, error) { + return nil, nil +} +func (m mockConnection) DynDial() (dynamic.Interface, error) { + return nil, nil +} +func (m mockConnection) HasMetrics() bool { + return false +} +func (m mockConnection) ValidNamespaceNames() (client.NamespaceNames, error) { + return nil, nil +} +func (m mockConnection) IsValidNamespace(string) bool { + return true +} +func (m mockConnection) ServerVersion() (*version.Info, error) { + return nil, nil +} +func (m mockConnection) CheckConnectivity() bool { + return false +} +func (m mockConnection) ActiveContext() string { + return "" +} +func (m mockConnection) ActiveNamespace() string { + return "" +} +func (m mockConnection) IsActiveNamespace(string) bool { + return false +} diff --git a/internal/config/mock_connection_test.go b/internal/config/mock_connection_test.go deleted file mode 100644 index 77f4568b07..0000000000 --- a/internal/config/mock_connection_test.go +++ /dev/null @@ -1,674 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/client (interfaces: Connection) - -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package config_test - -import ( - client "github.com/derailed/k9s/internal/client" - pegomock "github.com/petergtz/pegomock" - v1 "k8s.io/api/core/v1" - version "k8s.io/apimachinery/pkg/version" - disk "k8s.io/client-go/discovery/cached/disk" - dynamic "k8s.io/client-go/dynamic" - kubernetes "k8s.io/client-go/kubernetes" - rest "k8s.io/client-go/rest" - versioned "k8s.io/metrics/pkg/client/clientset/versioned" - "reflect" - "time" -) - -type MockConnection struct { - fail func(message string, callerSkip ...int) -} - -func NewMockConnection(options ...pegomock.Option) *MockConnection { - mock := &MockConnection{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockConnection) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockConnection) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockConnection) ActiveCluster() string { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ActiveCluster", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()}) - var ret0 string - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - } - return ret0 -} - -func (mock *MockConnection) ActiveNamespace() string { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ActiveNamespace", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()}) - var ret0 string - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - } - return ret0 -} - -func (mock *MockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CachedDiscovery", params, []reflect.Type{reflect.TypeOf((**disk.CachedDiscoveryClient)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *disk.CachedDiscoveryClient - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*disk.CachedDiscoveryClient) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) CanI(_param0 string, _param1 string, _param2 []string) (bool, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0, _param1, _param2} - result := pegomock.GetGenericMockFrom(mock).Invoke("CanI", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 bool - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) CheckConnectivity() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CheckConnectivity", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) Config() *client.Config { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("Config", params, []reflect.Type{reflect.TypeOf((**client.Config)(nil)).Elem()}) - var ret0 *client.Config - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*client.Config) - } - } - return ret0 -} - -func (mock *MockConnection) ConnectionOK() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ConnectionOK", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) Dial() (kubernetes.Interface, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("Dial", params, []reflect.Type{reflect.TypeOf((*kubernetes.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 kubernetes.Interface - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(kubernetes.Interface) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) DialLogs() (kubernetes.Interface, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("DialLogs", params, []reflect.Type{reflect.TypeOf((*kubernetes.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 kubernetes.Interface - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(kubernetes.Interface) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) DynDial() (dynamic.Interface, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("DynDial", params, []reflect.Type{reflect.TypeOf((*dynamic.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 dynamic.Interface - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(dynamic.Interface) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) HasMetrics() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("HasMetrics", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) IsActiveNamespace(_param0 string) bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("IsActiveNamespace", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) MXDial() (*versioned.Clientset, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("MXDial", params, []reflect.Type{reflect.TypeOf((**versioned.Clientset)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *versioned.Clientset - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*versioned.Clientset) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) RestConfig() (*rest.Config, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("RestConfig", params, []reflect.Type{reflect.TypeOf((**rest.Config)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *rest.Config - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*rest.Config) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) ServerVersion() (*version.Info, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ServerVersion", params, []reflect.Type{reflect.TypeOf((**version.Info)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *version.Info - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*version.Info) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) SwitchContext(_param0 string) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("SwitchContext", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(error) - } - } - return ret0 -} - -func (mock *MockConnection) ValidNamespaces() ([]v1.Namespace, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ValidNamespaces", params, []reflect.Type{reflect.TypeOf((*[]v1.Namespace)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 []v1.Namespace - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].([]v1.Namespace) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) VerifyWasCalledOnce() *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockConnection) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockConnection) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockConnection) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockConnection struct { - mock *MockConnection - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockConnection) ActiveCluster() *MockConnection_ActiveCluster_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ActiveCluster", params, verifier.timeout) - return &MockConnection_ActiveCluster_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ActiveCluster_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ActiveCluster_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ActiveCluster_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) ActiveNamespace() *MockConnection_ActiveNamespace_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ActiveNamespace", params, verifier.timeout) - return &MockConnection_ActiveNamespace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ActiveNamespace_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ActiveNamespace_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ActiveNamespace_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CachedDiscovery() *MockConnection_CachedDiscovery_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CachedDiscovery", params, verifier.timeout) - return &MockConnection_CachedDiscovery_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CachedDiscovery_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CachedDiscovery_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CachedDiscovery_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CanI(_param0 string, _param1 string, _param2 []string) *MockConnection_CanI_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CanI", params, verifier.timeout) - return &MockConnection_CanI_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CanI_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CanI_OngoingVerification) GetCapturedArguments() (string, string, []string) { - _param0, _param1, _param2 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1] -} - -func (c *MockConnection_CanI_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 [][]string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - _param1 = make([]string, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(string) - } - _param2 = make([][]string, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.([]string) - } - } - return -} - -func (verifier *VerifierMockConnection) CheckConnectivity() *MockConnection_CheckConnectivity_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckConnectivity", params, verifier.timeout) - return &MockConnection_CheckConnectivity_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CheckConnectivity_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CheckConnectivity_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CheckConnectivity_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) Config() *MockConnection_Config_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Config", params, verifier.timeout) - return &MockConnection_Config_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_Config_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_Config_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_Config_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) ConnectionOK() *MockConnection_ConnectionOK_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ConnectionOK", params, verifier.timeout) - return &MockConnection_ConnectionOK_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ConnectionOK_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ConnectionOK_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ConnectionOK_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) Dial() *MockConnection_Dial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Dial", params, verifier.timeout) - return &MockConnection_Dial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_Dial_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_Dial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_Dial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) DynDial() *MockConnection_DynDial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DynDial", params, verifier.timeout) - return &MockConnection_DynDial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_DynDial_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_DynDial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_DynDial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) HasMetrics() *MockConnection_HasMetrics_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasMetrics", params, verifier.timeout) - return &MockConnection_HasMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_HasMetrics_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_HasMetrics_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_HasMetrics_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) IsActiveNamespace(_param0 string) *MockConnection_IsActiveNamespace_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "IsActiveNamespace", params, verifier.timeout) - return &MockConnection_IsActiveNamespace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_IsActiveNamespace_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_IsActiveNamespace_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_IsActiveNamespace_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockConnection) MXDial() *MockConnection_MXDial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "MXDial", params, verifier.timeout) - return &MockConnection_MXDial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_MXDial_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_MXDial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_MXDial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) RestConfig() *MockConnection_RestConfig_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RestConfig", params, verifier.timeout) - return &MockConnection_RestConfig_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_RestConfig_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_RestConfig_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_RestConfig_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) ServerVersion() *MockConnection_ServerVersion_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ServerVersion", params, verifier.timeout) - return &MockConnection_ServerVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ServerVersion_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ServerVersion_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ServerVersion_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) SwitchContext(_param0 string) *MockConnection_SwitchContext_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SwitchContext", params, verifier.timeout) - return &MockConnection_SwitchContext_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_SwitchContext_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_SwitchContext_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_SwitchContext_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockConnection) ValidNamespaces() *MockConnection_ValidNamespaces_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ValidNamespaces", params, verifier.timeout) - return &MockConnection_ValidNamespaces_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ValidNamespaces_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ValidNamespaces_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ValidNamespaces_OngoingVerification) GetAllCapturedArguments() { -} diff --git a/internal/config/mock_kubesettings_test.go b/internal/config/mock_kubesettings_test.go deleted file mode 100644 index 68d7fd0901..0000000000 --- a/internal/config/mock_kubesettings_test.go +++ /dev/null @@ -1,252 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/config (interfaces: KubeSettings) - -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package config_test - -import ( - pegomock "github.com/petergtz/pegomock" - v1 "k8s.io/api/core/v1" - "reflect" - "time" -) - -type MockKubeSettings struct { - fail func(message string, callerSkip ...int) -} - -func NewMockKubeSettings(options ...pegomock.Option) *MockKubeSettings { - mock := &MockKubeSettings{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockKubeSettings) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockKubeSettings) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockKubeSettings) ClusterNames() (map[string]struct{}, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockKubeSettings().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ClusterNames", params, []reflect.Type{reflect.TypeOf((*map[string]struct{})(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 map[string]struct{} - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(map[string]struct{}) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockKubeSettings) CurrentClusterName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockKubeSettings().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CurrentClusterName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockKubeSettings) CurrentContextName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockKubeSettings().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CurrentContextName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockKubeSettings) CurrentNamespaceName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockKubeSettings().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CurrentNamespaceName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockKubeSettings) NamespaceNames(_param0 []v1.Namespace) []string { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockKubeSettings().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("NamespaceNames", params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem()}) - var ret0 []string - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].([]string) - } - } - return ret0 -} - -func (mock *MockKubeSettings) VerifyWasCalledOnce() *VerifierMockKubeSettings { - return &VerifierMockKubeSettings{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockKubeSettings) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockKubeSettings { - return &VerifierMockKubeSettings{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockKubeSettings) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockKubeSettings { - return &VerifierMockKubeSettings{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockKubeSettings) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockKubeSettings { - return &VerifierMockKubeSettings{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockKubeSettings struct { - mock *MockKubeSettings - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockKubeSettings) ClusterNames() *MockKubeSettings_ClusterNames_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ClusterNames", params, verifier.timeout) - return &MockKubeSettings_ClusterNames_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockKubeSettings_ClusterNames_OngoingVerification struct { - mock *MockKubeSettings - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockKubeSettings_ClusterNames_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockKubeSettings_ClusterNames_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockKubeSettings) CurrentClusterName() *MockKubeSettings_CurrentClusterName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CurrentClusterName", params, verifier.timeout) - return &MockKubeSettings_CurrentClusterName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockKubeSettings_CurrentClusterName_OngoingVerification struct { - mock *MockKubeSettings - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockKubeSettings_CurrentClusterName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockKubeSettings_CurrentClusterName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockKubeSettings) CurrentContextName() *MockKubeSettings_CurrentContextName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CurrentContextName", params, verifier.timeout) - return &MockKubeSettings_CurrentContextName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockKubeSettings_CurrentContextName_OngoingVerification struct { - mock *MockKubeSettings - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockKubeSettings_CurrentContextName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockKubeSettings_CurrentContextName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockKubeSettings) CurrentNamespaceName() *MockKubeSettings_CurrentNamespaceName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CurrentNamespaceName", params, verifier.timeout) - return &MockKubeSettings_CurrentNamespaceName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockKubeSettings_CurrentNamespaceName_OngoingVerification struct { - mock *MockKubeSettings - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockKubeSettings_CurrentNamespaceName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockKubeSettings_CurrentNamespaceName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockKubeSettings) NamespaceNames(_param0 []v1.Namespace) *MockKubeSettings_NamespaceNames_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "NamespaceNames", params, verifier.timeout) - return &MockKubeSettings_NamespaceNames_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockKubeSettings_NamespaceNames_OngoingVerification struct { - mock *MockKubeSettings - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockKubeSettings_NamespaceNames_OngoingVerification) GetCapturedArguments() []v1.Namespace { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockKubeSettings_NamespaceNames_OngoingVerification) GetAllCapturedArguments() (_param0 [][]v1.Namespace) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([][]v1.Namespace, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.([]v1.Namespace) - } - } - return -} diff --git a/internal/config/ns_test.go b/internal/config/ns_test.go deleted file mode 100644 index c84ffe54cf..0000000000 --- a/internal/config/ns_test.go +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package config_test - -import ( - "fmt" - "testing" - - "github.com/derailed/k9s/internal/config" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" -) - -func TestNSValidate(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2", "default"}) - - ns := config.NewNamespace() - ns.Validate(mc, mk) - - mk.VerifyWasCalledOnce() - assert.Equal(t, "default", ns.Active) - assert.Equal(t, []string{"default"}, ns.Favorites) -} - -func TestNSValidateMissing(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - mk := NewMockKubeSettings() - - ns := config.NewNamespace() - ns.Validate(mc, mk) - - assert.Equal(t, "default", ns.Active) - assert.Equal(t, []string{"default"}, ns.Favorites) -} - -func TestNSValidateNoNS(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), fmt.Errorf("Crap!")) - mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2"}) - - ns := config.NewNamespace() - ns.Validate(mc, mk) - - mk.VerifyWasCalledOnce() - assert.Equal(t, "default", ns.Active) - assert.Equal(t, []string{"default"}, ns.Favorites) -} - -func TestNSSetActive(t *testing.T) { - allNS := []string{"ns4", "ns3", "ns2", "ns1", "all", "default"} - uu := []struct { - ns string - fav []string - }{ - {"all", []string{"all", "default"}}, - {"ns1", []string{"ns1", "all", "default"}}, - {"ns2", []string{"ns2", "ns1", "all", "default"}}, - {"ns3", []string{"ns3", "ns2", "ns1", "all", "default"}}, - {"ns4", allNS}, - } - - mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn(allNS) - - ns := config.NewNamespace() - for _, u := range uu { - err := ns.SetActive(u.ns, mk) - - assert.Nil(t, err) - assert.Equal(t, u.ns, ns.Active) - assert.Equal(t, u.fav, ns.Favorites) - } -} - -func TestNSValidateRmFavs(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - mk := NewMockKubeSettings() - - ns := config.NewNamespace() - ns.Favorites = []string{"default", "fred", "blee"} - ns.Validate(mc, mk) - - assert.Equal(t, []string{"default", "fred"}, ns.Favorites) -} diff --git a/internal/config/plugin.go b/internal/config/plugin.go index 99d9c0cf85..b947f8663e 100644 --- a/internal/config/plugin.go +++ b/internal/config/plugin.go @@ -4,6 +4,7 @@ package config import ( + "errors" "fmt" "os" "path/filepath" @@ -13,13 +14,11 @@ import ( "gopkg.in/yaml.v2" ) -// K9sPluginsFilePath manages K9s plugins. -var K9sPluginsFilePath = YamlExtension(filepath.Join(K9sHome(), "plugin.yml")) -var K9sPluginDirectory = filepath.Join("k9s", "plugins") +const k9sPluginsDir = "k9s/plugins" // Plugins represents a collection of plugins. type Plugins struct { - Plugin map[string]Plugin `yaml:"plugin"` + Plugins map[string]Plugin `yaml:"plugins"` } // Plugin describes a K9s plugin. @@ -41,22 +40,58 @@ func (p Plugin) String() string { // NewPlugins returns a new plugin. func NewPlugins() Plugins { return Plugins{ - Plugin: make(map[string]Plugin), + Plugins: make(map[string]Plugin), } } // Load K9s plugins. -func (p Plugins) Load() error { - pluginDirs := make([]string, 0, len(xdg.DataDirs)) +func (p Plugins) Load(path string) error { + var errs error + if err := p.load(AppPluginsFile); err != nil { + errs = errors.Join(errs, err) + } + if err := p.load(path); err != nil { + errs = errors.Join(errs, err) + } + for _, dataDir := range xdg.DataDirs { - pluginDirs = append(pluginDirs, filepath.Join(dataDir, K9sPluginDirectory)) + if err := p.loadPluginDir(filepath.Join(dataDir, k9sPluginsDir)); err != nil { + errs = errors.Join(errs, err) + } } - return p.LoadPlugins(K9sPluginsFilePath, pluginDirs) + return errs } -// LoadPlugins loads plugins from a given file and a set of plugin directories. -func (p Plugins) LoadPlugins(path string, pluginDirs []string) error { +func (p Plugins) loadPluginDir(dir string) error { + pluginFiles, err := os.ReadDir(dir) + if err != nil { + return nil + } + + var errs error + for _, file := range pluginFiles { + if file.IsDir() || !isYamlFile(file.Name()) { + continue + } + pluginFile, err := os.ReadFile(filepath.Join(dir, file.Name())) + if err != nil { + errs = errors.Join(errs, err) + } + var plugin Plugin + if err = yaml.Unmarshal(pluginFile, &plugin); err != nil { + return err + } + p.Plugins[strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))] = plugin + } + + return errs +} + +func (p *Plugins) load(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil + } f, err := os.ReadFile(path) if err != nil { return err @@ -66,29 +101,8 @@ func (p Plugins) LoadPlugins(path string, pluginDirs []string) error { if err := yaml.Unmarshal(f, &pp); err != nil { return err } - for k, v := range pp.Plugin { - p.Plugin[k] = v - } - - for _, pluginDir := range pluginDirs { - pluginFiles, err := os.ReadDir(pluginDir) - if err != nil { - continue - } - for _, file := range pluginFiles { - if file.IsDir() || !isYamlFile(file.Name()) { - continue - } - pluginFile, err := os.ReadFile(filepath.Join(pluginDir, file.Name())) - if err != nil { - return err - } - var plugin Plugin - if err = yaml.Unmarshal(pluginFile, &plugin); err != nil { - return err - } - p.Plugin[strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))] = plugin - } + for k, v := range pp.Plugins { + p.Plugins[k] = v } return nil diff --git a/internal/config/plugin_test.go b/internal/config/plugin_test.go index 175a8285ab..f0870e0142 100644 --- a/internal/config/plugin_test.go +++ b/internal/config/plugin_test.go @@ -1,16 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s -package config_test +package config import ( "testing" - "github.com/derailed/k9s/internal/config" "github.com/stretchr/testify/assert" ) -var pluginYmlTestData = config.Plugin{ +var pluginYmlTestData = Plugin{ Scopes: []string{"po", "dp"}, Args: []string{"-n", "$NAMESPACE", "-boolean"}, ShortCut: "shift-s", @@ -20,7 +19,7 @@ var pluginYmlTestData = config.Plugin{ Background: false, } -var test1YmlTestData = config.Plugin{ +var test1YmlTestData = Plugin{ Scopes: []string{"po", "dp"}, Args: []string{"-n", "$NAMESPACE", "-boolean"}, ShortCut: "shift-s", @@ -30,7 +29,7 @@ var test1YmlTestData = config.Plugin{ Background: false, } -var test2YmlTestData = config.Plugin{ +var test2YmlTestData = Plugin{ Scopes: []string{"svc", "ing"}, Args: []string{"-n", "$NAMESPACE", "-oyaml"}, ShortCut: "shift-r", @@ -41,29 +40,31 @@ var test2YmlTestData = config.Plugin{ } func TestSinglePluginFileLoad(t *testing.T) { - p := config.NewPlugins() - assert.Nil(t, p.LoadPlugins("testdata/plugin.yml", []string{"/random/dir/not/exist"})) + p := NewPlugins() + assert.Nil(t, p.load("testdata/plugins.yaml")) + assert.Nil(t, p.loadPluginDir("/random/dir/not/exist")) - assert.Equal(t, 1, len(p.Plugin)) - k, ok := p.Plugin["blah"] + assert.Equal(t, 1, len(p.Plugins)) + k, ok := p.Plugins["blah"] assert.True(t, ok) assert.ObjectsAreEqual(pluginYmlTestData, k) } func TestMultiplePluginFilesLoad(t *testing.T) { - p := config.NewPlugins() - assert.Nil(t, p.LoadPlugins("testdata/plugin.yml", []string{"testdata/plugins"})) + p := NewPlugins() + assert.Nil(t, p.load("testdata/plugins.yaml")) + assert.Nil(t, p.loadPluginDir("testdata/plugins")) - testPlugins := map[string]config.Plugin{ + testPlugins := map[string]Plugin{ "blah": pluginYmlTestData, "test1": test1YmlTestData, "test2": test2YmlTestData, } - assert.Equal(t, len(testPlugins), len(p.Plugin)) + assert.Equal(t, len(testPlugins), len(p.Plugins)) for name, expectedPlugin := range testPlugins { - k, ok := p.Plugin[name] + k, ok := p.Plugins[name] assert.True(t, ok) assert.ObjectsAreEqual(expectedPlugin, k) } diff --git a/internal/config/scans.go b/internal/config/scans.go new file mode 100644 index 0000000000..19e970d93f --- /dev/null +++ b/internal/config/scans.go @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config + +// Labels tracks a collection of labels. +type Labels map[string][]string + +func (l Labels) exclude(k, val string) bool { + vv, ok := l[k] + if !ok { + return false + } + + for _, v := range vv { + if v == val { + return true + } + } + + return false +} + +// Blacklist tracks vul scan exclusions. +type BlackList struct { + Namespaces []string `yaml:"namespaces"` + Labels Labels `yaml:"labels"` +} + +func newBlackList() BlackList { + return BlackList{ + Labels: make(Labels), + } +} + +func (b BlackList) exclude(ns string, ll map[string]string) bool { + for _, nss := range b.Namespaces { + if nss == ns { + return true + } + } + for k, v := range ll { + if b.Labels.exclude(k, v) { + return true + } + } + + return false +} + +// ImageScans tracks vul scans options. +type ImageScans struct { + Enable bool `yaml:"enable"` + BlackList BlackList `yaml:"blackList"` +} + +// NewImageScans returns a new instance. +func NewImageScans() *ImageScans { + return &ImageScans{ + BlackList: newBlackList(), + } +} + +// ShouldExclude checks if scan should be excluder given ns/labels +func (i *ImageScans) ShouldExclude(ns string, ll map[string]string) bool { + if !i.Enable { + return false + } + + return i.BlackList.exclude(ns, ll) +} diff --git a/internal/config/shell_pod.go b/internal/config/shell_pod.go index 919cedae76..19ad05a0c2 100644 --- a/internal/config/shell_pod.go +++ b/internal/config/shell_pod.go @@ -5,6 +5,7 @@ package config import ( "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config/data" v1 "k8s.io/api/core/v1" ) @@ -36,7 +37,7 @@ func NewShellPod() *ShellPod { } // Validate validates the configuration. -func (s *ShellPod) Validate(client.Connection, KubeSettings) { +func (s *ShellPod) Validate(client.Connection, data.KubeSettings) { if s.Image == "" { s.Image = defaultDockerShellImage } diff --git a/internal/config/styles.go b/internal/config/styles.go index cefefcf085..2ad1956ca3 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -5,16 +5,12 @@ package config import ( "os" - "path/filepath" "github.com/derailed/tcell/v2" "github.com/derailed/tview" "gopkg.in/yaml.v2" ) -// K9sStylesFile represents K9s skins file location. -var K9sStylesFile = YamlExtension(filepath.Join(K9sHome(), "skin.yml")) - // StyleListener represents a skin's listener. type StyleListener interface { // StylesChanged notifies listener the skin changed. @@ -434,6 +430,11 @@ func newMenu() Menu { // NewStyles creates a new default config. func NewStyles() *Styles { + var s Styles + if err := yaml.Unmarshal(stockSkinTpl, &s); err == nil { + return &s + } + return &Styles{ K9s: newStyle(), } @@ -446,7 +447,6 @@ func (s *Styles) Reset() { // DefaultSkin loads the default skin. func (s *Styles) DefaultSkin() { - s.K9s = newStyle() } // FgColor returns the foreground color. @@ -545,7 +545,6 @@ func (s *Styles) Load(path string) error { if err := yaml.Unmarshal(f, s); err != nil { return err } - // s.fireStylesChanged() return nil } diff --git a/internal/config/styles_test.go b/internal/config/styles_test.go index d3d8874a1e..572581c164 100644 --- a/internal/config/styles_test.go +++ b/internal/config/styles_test.go @@ -30,7 +30,7 @@ func TestColor(t *testing.T) { func TestSkinNone(t *testing.T) { s := config.NewStyles() - assert.Nil(t, s.Load("testdata/empty_skin.yml")) + assert.Nil(t, s.Load("testdata/empty_skin.yaml")) s.Update() assert.Equal(t, "#5f9ea0", s.Body().FgColor.String()) @@ -43,7 +43,7 @@ func TestSkinNone(t *testing.T) { func TestSkin(t *testing.T) { s := config.NewStyles() - assert.Nil(t, s.Load("testdata/black_and_wtf.yml")) + assert.Nil(t, s.Load("testdata/black_and_wtf.yaml")) s.Update() assert.Equal(t, "#ffffff", s.Body().FgColor.String()) @@ -56,10 +56,10 @@ func TestSkin(t *testing.T) { func TestSkinNotExits(t *testing.T) { s := config.NewStyles() - assert.NotNil(t, s.Load("testdata/blee.yml")) + assert.NotNil(t, s.Load("testdata/blee.yaml")) } func TestSkinBoarked(t *testing.T) { s := config.NewStyles() - assert.NotNil(t, s.Load("testdata/skin_boarked.yml")) + assert.NotNil(t, s.Load("testdata/skin_boarked.yaml")) } diff --git a/internal/config/templates/aliases.yaml b/internal/config/templates/aliases.yaml new file mode 100644 index 0000000000..81c781f970 --- /dev/null +++ b/internal/config/templates/aliases.yaml @@ -0,0 +1,9 @@ +aliases: + dp: apps/v1/deployments + sec: v1/secrets + jo: batch/v1/jobs + cr: rbac.authorization.k8s.io/v1/clusterroles + crb: rbac.authorization.k8s.io/v1/clusterrolebindings + ro: rbac.authorization.k8s.io/v1/roles + rb: rbac.authorization.k8s.io/v1/rolebindings + np: networking.k8s.io/v1/networkpolicies diff --git a/internal/config/templates/benchmarks.yaml b/internal/config/templates/benchmarks.yaml new file mode 100644 index 0000000000..9efba4cf3e --- /dev/null +++ b/internal/config/templates/benchmarks.yaml @@ -0,0 +1,4 @@ +benchmarks: + defaults: + concurrency: 2 + requests: 200 \ No newline at end of file diff --git a/internal/config/templates/hotkeys.yaml b/internal/config/templates/hotkeys.yaml new file mode 100644 index 0000000000..5c2723b673 --- /dev/null +++ b/internal/config/templates/hotkeys.yaml @@ -0,0 +1,6 @@ +hotKeys: + # Examples... + # shift-0: + # shortCut: Shift-0 + # description: View Workloads + # command: wk k8s-app=cilium \ No newline at end of file diff --git a/internal/config/templates/stock-skin.yaml b/internal/config/templates/stock-skin.yaml new file mode 100644 index 0000000000..02e68ab280 --- /dev/null +++ b/internal/config/templates/stock-skin.yaml @@ -0,0 +1,97 @@ +# ----------------------------------------------------------------------------- +# Stock skin +# ----------------------------------------------------------------------------- + +# Skin... +k9s: + body: + fgColor: cadetblue + bgColor: black + logoColor: orange + logoColorMsg: white + logoColorInfo: green + logoColorWarn: mediumvioletred + logoColorError: red + prompt: + fgColor: cadetblue + bgColor: black + suggestColor: dodgerblue + border: + default: seagreen + command: aqua + info: + fgColor: orange + sectionColor: white + dialog: + fgColor: dodgerblue + bgColor: black + buttonFgColor: black + buttonBgColor: dodgerblue + buttonFocusFgColor: white + buttonFocusBgColor: fuchsia + labelFgColor: fuchsia + fieldFgColor: dodgerblue + frame: + border: + fgColor: dodgerblue + focusColor: aqua + menu: + fgColor: white + keyColor: dodgerblue + numKeyColor: fuchsia + crumbs: + fgColor: black + bgColor: aqua + activeColor: orange + status: + newColor: lightskyblue + modifyColor: greenyellow + addColor: white + errorColor: orangered + pendingColor: darkorange + highlightColor: aqua + killColor: mediumpurple + completedColor: gray + title: + fgColor: aqua + highlightColor: fuchsia + counterColor: papayawhip + filterColor: steelblue + views: + # Charts skins... + charts: + bgColor: black + defaultDialColors: + - linegreen + - orangered + defaultChartColors: + - linegreen + - orangered + table: + fgColor: aqua + bgColor: black + cursorFgColor: white + cursorBgColor: black + markColor: darkgoldenrod + header: + fgColor: lightGray + bgColor: black + sorterColor: orange + xray: + fgColor: blue + bgColor: black + cursorColor: aqua + graphicColor: darkgoldenrod + showIcons: false + yaml: + keyColor: steelblue + colonColor: white + valueColor: papayawhip + logs: + fgColor: white + bgColor: black + indicator: + fgColor: dodgerblue + bgColor: black + toggleOnColor: limegreen + toggleOffColor: steelblue \ No newline at end of file diff --git a/internal/config/testdata/alias.yml b/internal/config/testdata/alias.yaml similarity index 83% rename from internal/config/testdata/alias.yml rename to internal/config/testdata/alias.yaml index 10835dee0e..185113e7da 100644 --- a/internal/config/testdata/alias.yml +++ b/internal/config/testdata/alias.yaml @@ -1,3 +1,3 @@ -alias: +aliases: dp: "apps.v1.deployments" pe: ".v1.pods" diff --git a/internal/config/testdata/b_containers.yml b/internal/config/testdata/b_containers.yaml similarity index 100% rename from internal/config/testdata/b_containers.yml rename to internal/config/testdata/b_containers.yaml diff --git a/internal/config/testdata/b_containers_1.yml b/internal/config/testdata/b_containers_1.yaml similarity index 100% rename from internal/config/testdata/b_containers_1.yml rename to internal/config/testdata/b_containers_1.yaml diff --git a/internal/config/testdata/b_good.yml b/internal/config/testdata/b_good.yaml similarity index 100% rename from internal/config/testdata/b_good.yml rename to internal/config/testdata/b_good.yaml diff --git a/internal/config/testdata/b_toast.yml b/internal/config/testdata/b_toast.yaml similarity index 100% rename from internal/config/testdata/b_toast.yml rename to internal/config/testdata/b_toast.yaml diff --git a/internal/config/testdata/bench-fred.yml b/internal/config/testdata/bench-fred.yaml similarity index 100% rename from internal/config/testdata/bench-fred.yml rename to internal/config/testdata/bench-fred.yaml diff --git a/internal/config/testdata/black_and_wtf.yml b/internal/config/testdata/black_and_wtf.yaml similarity index 100% rename from internal/config/testdata/black_and_wtf.yml rename to internal/config/testdata/black_and_wtf.yaml diff --git a/internal/config/testdata/empty_skin.yml b/internal/config/testdata/empty_skin.yaml similarity index 100% rename from internal/config/testdata/empty_skin.yml rename to internal/config/testdata/empty_skin.yaml diff --git a/internal/config/testdata/hot_key.yml b/internal/config/testdata/hotkeys.yaml similarity index 90% rename from internal/config/testdata/hot_key.yml rename to internal/config/testdata/hotkeys.yaml index 81f16319e4..255555b3e1 100644 --- a/internal/config/testdata/hot_key.yml +++ b/internal/config/testdata/hotkeys.yaml @@ -1,4 +1,4 @@ -hotKey: +hotKeys: pods: shortCut: shift-0 description: Launch pod view diff --git a/internal/config/testdata/k8s.yml b/internal/config/testdata/k8s.yaml similarity index 100% rename from internal/config/testdata/k8s.yml rename to internal/config/testdata/k8s.yaml diff --git a/internal/config/testdata/k9s.yml b/internal/config/testdata/k9s.yaml similarity index 91% rename from internal/config/testdata/k9s.yml rename to internal/config/testdata/k9s.yaml index e98a2180cf..8bb5df2e1e 100644 --- a/internal/config/testdata/k9s.yml +++ b/internal/config/testdata/k9s.yaml @@ -6,9 +6,9 @@ k9s: tail: 200 buffer: 2000 currentContext: minikube - currentCluster: minikube - clusters: + contexts: minikube: + cluster: minikube namespace: active: kube-system favorites: @@ -20,6 +20,7 @@ k9s: view: active: ctx fred: + cluster: fred namespace: active: default favorites: diff --git a/internal/config/testdata/k9s1.yml b/internal/config/testdata/k9s1.yaml similarity index 100% rename from internal/config/testdata/k9s1.yml rename to internal/config/testdata/k9s1.yaml diff --git a/internal/config/testdata/k9s_old.yml b/internal/config/testdata/k9s_old.yaml similarity index 100% rename from internal/config/testdata/k9s_old.yml rename to internal/config/testdata/k9s_old.yaml diff --git a/internal/config/testdata/k9s_readonly.yml b/internal/config/testdata/k9s_readonly.yaml similarity index 93% rename from internal/config/testdata/k9s_readonly.yml rename to internal/config/testdata/k9s_readonly.yaml index e8c5c1928b..e3edca141e 100644 --- a/internal/config/testdata/k9s_readonly.yml +++ b/internal/config/testdata/k9s_readonly.yaml @@ -5,8 +5,7 @@ k9s: tail: 200 buffer: 2000 currentContext: minikube - currentCluster: minikube - clusters: + contexts: minikube: namespace: active: kube-system diff --git a/internal/config/testdata/k9s_toast.yml b/internal/config/testdata/k9s_toast.yaml similarity index 92% rename from internal/config/testdata/k9s_toast.yml rename to internal/config/testdata/k9s_toast.yaml index 189706a655..ac84a5893f 100644 --- a/internal/config/testdata/k9s_toast.yml +++ b/internal/config/testdata/k9s_toast.yaml @@ -2,8 +2,7 @@ k9s: refreshRate: 2 logBufferSize: 200 currentContext: minikube - currentCluster: minikube - clusters: + contexts: minikube: namespace: active: kube-system diff --git a/internal/config/testdata/kubeconfig-test.yml b/internal/config/testdata/kubeconfig-test.yaml similarity index 73% rename from internal/config/testdata/kubeconfig-test.yml rename to internal/config/testdata/kubeconfig-test.yaml index a3c72a7af9..725d04bc7d 100644 --- a/internal/config/testdata/kubeconfig-test.yml +++ b/internal/config/testdata/kubeconfig-test.yaml @@ -4,19 +4,19 @@ clusters: - cluster: certificate-authority: /Users/test/ca.crt server: https://1.2.3.4:8443 - name: testCluster + name: cl-1 contexts: - context: - cluster: cluster1 + cluster: cl-1 user: user1 - namespace: ns1 - name: test1 + namespace: ns-1 + name: ct-1 - context: - cluster: cluster2 + cluster: cl-1 user: user2 - namespace: ns2 - name: test2 -current-context: test1 + namespace: ns-2 + name: ct-2 +current-context: ct-1 preferences: {} users: - name: user1 diff --git a/internal/config/testdata/plugin.yml b/internal/config/testdata/plugins.yaml similarity index 95% rename from internal/config/testdata/plugin.yml rename to internal/config/testdata/plugins.yaml index 0563f6f827..cfa4748967 100644 --- a/internal/config/testdata/plugin.yml +++ b/internal/config/testdata/plugins.yaml @@ -1,4 +1,4 @@ -plugin: +plugins: blah: shortCut: shift-s confirm: true diff --git a/internal/config/testdata/plugins/test1.yml b/internal/config/testdata/plugins/test1.yaml similarity index 100% rename from internal/config/testdata/plugins/test1.yml rename to internal/config/testdata/plugins/test1.yaml diff --git a/internal/config/testdata/plugins/test2.yml b/internal/config/testdata/plugins/test2.yaml similarity index 100% rename from internal/config/testdata/plugins/test2.yml rename to internal/config/testdata/plugins/test2.yaml diff --git a/internal/config/testdata/skin_boarked.yml b/internal/config/testdata/skin_boarked.yaml similarity index 100% rename from internal/config/testdata/skin_boarked.yml rename to internal/config/testdata/skin_boarked.yaml diff --git a/internal/config/testdata/view_settings.yml b/internal/config/testdata/view_settings.yaml similarity index 100% rename from internal/config/testdata/view_settings.yml rename to internal/config/testdata/view_settings.yaml diff --git a/internal/config/threshold.go b/internal/config/threshold.go index f3300178bf..f15bd8c1fa 100644 --- a/internal/config/threshold.go +++ b/internal/config/threshold.go @@ -5,6 +5,7 @@ package config import ( "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config/data" ) const ( @@ -65,7 +66,7 @@ func NewThreshold() Threshold { } // Validate a namespace is setup correctly. -func (t Threshold) Validate(c client.Connection, ks KubeSettings) { +func (t Threshold) Validate(c client.Connection, ks data.KubeSettings) { for _, k := range []string{"cpu", "memory"} { v, ok := t[k] if !ok { diff --git a/internal/config/types.go b/internal/config/types.go new file mode 100644 index 0000000000..9e8fea59b6 --- /dev/null +++ b/internal/config/types.go @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config + +const ( + defaultRefreshRate = 2 + defaultMaxConnRetry = 5 +) + +// UI tracks ui specific configs. +type UI struct { + // EnableMouse toggles mouse support. + EnableMouse bool `yaml:"enableMouse"` + + // Headless toggles top header display. + Headless bool `yaml:"headless"` + + // LogoLess toggles k9s logo. + Logoless bool `yaml:"logoless"` + + // Crumbsless toggles nav crumb display. + Crumbsless bool `yaml:"crumbsless"` + + // NoIcons toggles icons display. + NoIcons bool `yaml:"noIcons"` + + // Skin reference the general k9s skin name. + // Can be overridden per context. + Skin string `yaml:"skin,omitempty"` +} diff --git a/internal/config/views.go b/internal/config/views.go index 74907e4347..ae2ad4798e 100644 --- a/internal/config/views.go +++ b/internal/config/views.go @@ -5,17 +5,13 @@ package config import ( "os" - "path/filepath" "gopkg.in/yaml.v2" ) -// K9sViewConfigFile represents the location for the views configuration. -var K9sViewConfigFile = YamlExtension(filepath.Join(K9sHome(), "views.yml")) - // ViewConfigListener represents a view config listener. type ViewConfigListener interface { - // ConfigChanged notifies listener the view configuration changed. + // ViewSettingsChanged notifies listener the view configuration changed. ViewSettingsChanged(ViewSetting) } @@ -90,6 +86,8 @@ func (v *CustomView) fireConfigChanged() { for gvr, list := range v.listeners { if v, ok := v.K9s.Views[gvr]; ok { list.ViewSettingsChanged(v) + } else { + list.ViewSettingsChanged(ViewSetting{}) } } } diff --git a/internal/config/views_test.go b/internal/config/views_test.go index af3885fe77..c883fed95e 100644 --- a/internal/config/views_test.go +++ b/internal/config/views_test.go @@ -13,7 +13,7 @@ import ( func TestViewSettingsLoad(t *testing.T) { cfg := config.NewCustomView() - assert.Nil(t, cfg.Load("testdata/view_settings.yml")) + assert.Nil(t, cfg.Load("testdata/view_settings.yaml")) assert.Equal(t, 1, len(cfg.K9s.Views)) assert.Equal(t, 4, len(cfg.K9s.Views["v1/pods"].Columns)) } diff --git a/internal/dao/alias.go b/internal/dao/alias.go index bce58361a0..f07b86c655 100644 --- a/internal/dao/alias.go +++ b/internal/dao/alias.go @@ -14,6 +14,7 @@ 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" ) @@ -22,21 +23,23 @@ var _ Accessor = (*Alias)(nil) // Alias tracks standard and custom command aliases. type Alias struct { NonResource + *config.Aliases } // NewAlias returns a new set of aliases. func NewAlias(f Factory) *Alias { - a := Alias{Aliases: config.NewAliases()} + a := Alias{ + Aliases: config.NewAliases(), + } a.Init(f, client.NewGVR("aliases")) return &a } // Check verifies an alias is defined for this command. -func (a *Alias) Check(cmd string) bool { - _, ok := a.Aliases.Get(cmd) - return ok +func (a *Alias) Check(cmd string) (string, bool) { + return a.Aliases.Get(cmd) } // List returns a collection of aliases. @@ -49,19 +52,30 @@ func (a *Alias) List(ctx context.Context, _ string) ([]runtime.Object, error) { oo := make([]runtime.Object, 0, len(m)) for gvr, aliases := range m { sort.StringSlice(aliases).Sort() - oo = append(oo, render.AliasRes{GVR: gvr, Aliases: aliases}) + oo = append(oo, render.AliasRes{ + GVR: gvr, + Aliases: aliases, + }) } return oo, nil } // AsGVR returns a matching gvr if it exists. -func (a *Alias) AsGVR(cmd string) (client.GVR, bool) { - gvr, ok := a.Aliases.Get(cmd) - if ok { - return client.NewGVR(gvr), true +func (a *Alias) AsGVR(c string) (client.GVR, string, bool) { + exp, ok := a.Aliases.Get(c) + if !ok { + return client.NoGVR, "", ok } - return client.GVR{}, false + p := cmd.NewInterpreter(exp) + if strings.Contains(p.Cmd(), "/") { + return client.NewGVR(p.Cmd()), "", true + } + if gvr, ok := a.Aliases.Get(p.Cmd()); ok { + return client.NewGVR(gvr), exp, true + } + + return client.NoGVR, "", false } // Get fetch a resource. @@ -70,15 +84,15 @@ func (a *Alias) Get(_ context.Context, _ string) (runtime.Object, error) { } // Ensure makes sure alias are loaded. -func (a *Alias) Ensure() (config.Alias, error) { +func (a *Alias) Ensure(path string) (config.Alias, error) { if err := MetaAccess.LoadResources(a.Factory); err != nil { return config.Alias{}, err } - return a.Alias, a.load() + return a.Alias, a.load(path) } -func (a *Alias) load() error { - if err := a.Load(); err != nil { +func (a *Alias) load(path string) error { + if err := a.Load(path); err != nil { return err } diff --git a/internal/dao/alias_test.go b/internal/dao/alias_test.go index ceab249822..f28b16db6f 100644 --- a/internal/dao/alias_test.go +++ b/internal/dao/alias_test.go @@ -19,6 +19,48 @@ import ( "k8s.io/client-go/informers" ) +func TestAsGVR(t *testing.T) { + a := dao.NewAlias(makeFactory()) + a.Aliases.Define("v1/pods", "po", "pod", "pods") + a.Aliases.Define("workloads", "workloads", "workload", "wkl") + + uu := map[string]struct { + cmd string + ok bool + gvr client.GVR + }{ + "ok": { + cmd: "pods", + ok: true, + gvr: client.NewGVR("v1/pods"), + }, + "ok-short": { + cmd: "po", + ok: true, + gvr: client.NewGVR("v1/pods"), + }, + "missing": { + cmd: "zorg", + }, + "alias": { + cmd: "wkl", + ok: true, + gvr: client.NewGVR("workloads"), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + gvr, _, ok := a.AsGVR(u.cmd) + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.gvr, gvr) + } + }) + } +} + func TestAliasList(t *testing.T) { a := dao.Alias{} a.Init(makeFactory(), client.NewGVR("aliases")) @@ -49,24 +91,24 @@ func makeAliases() *dao.Alias { type testFactory struct{} +func makeFactory() dao.Factory { + return testFactory{} +} + var _ dao.Factory = testFactory{} func (f testFactory) Client() client.Connection { return nil } - func (f testFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) { return nil, nil } - func (f testFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) { return nil, nil } - func (f testFactory) ForResource(ns, gvr string) (informers.GenericInformer, error) { return nil, nil } - func (f testFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { return nil, nil } @@ -75,7 +117,3 @@ func (f testFactory) Forwarders() watch.Forwarders { return nil } func (f testFactory) DeleteForwarder(string) {} - -func makeFactory() dao.Factory { - return testFactory{} -} diff --git a/internal/dao/benchmark.go b/internal/dao/benchmark.go index 75dcdc6a81..683cb2a4b2 100644 --- a/internal/dao/benchmark.go +++ b/internal/dao/benchmark.go @@ -45,20 +45,21 @@ func (b *Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error if !ok { return nil, errors.New("no benchmark dir found in context") } - path, _ := ctx.Value(internal.KeyPath).(string) + path, ok := ctx.Value(internal.KeyPath).(string) + if !ok { + return nil, errors.New("no path specified in context") + } + pathMatch := BenchRx.ReplaceAllString(strings.Replace(path, "/", "_", 1), "_") ff, err := os.ReadDir(dir) if err != nil { return nil, err } - - fileName := BenchRx.ReplaceAllString(strings.Replace(path, "/", "_", 1), "_") oo := make([]runtime.Object, 0, len(ff)) for _, f := range ff { - if path != "" && !strings.HasPrefix(f.Name(), fileName) { + if !strings.HasPrefix(f.Name(), pathMatch) { continue } - if fi, err := f.Info(); err == nil { oo = append(oo, render.BenchInfo{File: fi, Path: filepath.Join(dir, f.Name())}) } diff --git a/internal/dao/benchmark_test.go b/internal/dao/benchmark_test.go index 0d4aae08c8..12fed066d8 100644 --- a/internal/dao/benchmark_test.go +++ b/internal/dao/benchmark_test.go @@ -19,6 +19,7 @@ func TestBenchmarkList(t *testing.T) { a.Init(makeFactory(), client.NewGVR("benchmarks")) ctx := context.WithValue(context.Background(), internal.KeyDir, "testdata/bench") + ctx = context.WithValue(ctx, internal.KeyPath, "") oo, err := a.List(ctx, "-") assert.Nil(t, err) diff --git a/internal/dao/cluster.go b/internal/dao/cluster.go index 079e880450..6dcc8d25dd 100644 --- a/internal/dao/cluster.go +++ b/internal/dao/cluster.go @@ -19,7 +19,7 @@ type RefScanner interface { // Init initializes the scanner Init(Factory, client.GVR) // Scan scan the resource for references. - Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) + Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) } @@ -58,7 +58,7 @@ func ScanForRefs(ctx context.Context, f Factory) (Refs, error) { log.Debug().Msgf("Cluster Scan %v", time.Since(t)) }(time.Now()) - gvr, ok := ctx.Value(internal.KeyGVR).(string) + gvr, ok := ctx.Value(internal.KeyGVR).(client.GVR) if !ok { return nil, errors.New("expecting context GVR") } diff --git a/internal/dao/container_test.go b/internal/dao/container_test.go index 6765599942..25877dfd08 100644 --- a/internal/dao/container_test.go +++ b/internal/dao/container_test.go @@ -62,12 +62,14 @@ func (c *conn) ValidNamespaces() ([]v1.Namespace, error) { return n 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 string, verbs []string) (bool, error) { return true, nil } -func (c *conn) ActiveCluster() string { return "" } -func (c *conn) ActiveNamespace() string { return "" } -func (c *conn) IsActiveNamespace(string) bool { return false } +func (c *conn) ServerVersion() (*version.Info, error) { return nil, nil } +func (c *conn) CurrentNamespaceName() (string, error) { return "", nil } +func (c *conn) CanI(ns, gvr 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 } type podFactory struct{} diff --git a/internal/dao/cronjob.go b/internal/dao/cronjob.go index 290e1bb9ea..99ae897e2e 100644 --- a/internal/dao/cronjob.go +++ b/internal/dao/cronjob.go @@ -173,7 +173,7 @@ func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error { } // Scan scans for cluster resource refs. -func (c *CronJob) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) { +func (c *CronJob) Scan(ctx 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()) if err != nil { @@ -188,7 +188,7 @@ func (c *CronJob) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, e return nil, errors.New("expecting CronJob resource") } switch gvr { - case "v1/configmaps": + case CmGVR: if !hasConfigMap(&cj.Spec.JobTemplate.Spec.Template.Spec, n) { continue } @@ -196,7 +196,7 @@ func (c *CronJob) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, e GVR: c.GVR(), FQN: client.FQN(cj.Namespace, cj.Name), }) - case "v1/secrets": + case SecGVR: found, err := hasSecret(c.Factory, &cj.Spec.JobTemplate.Spec.Template.Spec, cj.Namespace, n, wait) if err != nil { log.Warn().Err(err).Msgf("locate secret %q", fqn) @@ -209,7 +209,7 @@ func (c *CronJob) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, e GVR: c.GVR(), FQN: client.FQN(cj.Namespace, cj.Name), }) - case "scheduling.k8s.io/v1/priorityclasses": + case PcGVR: if !hasPC(&cj.Spec.JobTemplate.Spec.Template.Spec, n) { continue } diff --git a/internal/dao/describe.go b/internal/dao/describe.go index 32db74b440..d990dfd405 100644 --- a/internal/dao/describe.go +++ b/internal/dao/describe.go @@ -26,7 +26,7 @@ func Describe(c client.Connection, gvr client.GVR, path string) (string, error) ns, n := client.Namespaced(path) if client.IsClusterScoped(ns) { - ns = client.AllNamespaces + ns = client.BlankNamespace } mapping, err := mapper.ResourceFor(gvr.AsResourceName(), gvk.Kind) if err != nil { diff --git a/internal/dao/dp.go b/internal/dao/dp.go index db1680aa2b..2eb5e56bbf 100644 --- a/internal/dao/dp.go +++ b/internal/dao/dp.go @@ -194,7 +194,7 @@ 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, fqn string, wait bool) (Refs, error) { +func (d *Deployment) Scan(ctx 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()) if err != nil { @@ -209,7 +209,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs return nil, errors.New("expecting Deployment resource") } switch gvr { - case "v1/configmaps": + case CmGVR: if !hasConfigMap(&dp.Spec.Template.Spec, n) { continue } @@ -217,7 +217,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs GVR: d.GVR(), FQN: client.FQN(dp.Namespace, dp.Name), }) - case "v1/secrets": + case SecGVR: found, err := hasSecret(d.Factory, &dp.Spec.Template.Spec, dp.Namespace, n, wait) if err != nil { log.Warn().Err(err).Msgf("scanning secret %q", fqn) @@ -230,7 +230,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs GVR: d.GVR(), FQN: client.FQN(dp.Namespace, dp.Name), }) - case "v1/persistentvolumeclaims": + case PvcGVR: if !hasPVC(&dp.Spec.Template.Spec, n) { continue } @@ -238,7 +238,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs GVR: d.GVR(), FQN: client.FQN(dp.Namespace, dp.Name), }) - case "scheduling.k8s.io/v1/priorityclasses": + case PcGVR: if !hasPC(&dp.Spec.Template.Spec, n) { continue } diff --git a/internal/dao/ds.go b/internal/dao/ds.go index 44c84e3915..0850a8ab3e 100644 --- a/internal/dao/ds.go +++ b/internal/dao/ds.go @@ -214,7 +214,7 @@ 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, fqn string, wait bool) (Refs, error) { +func (d *DaemonSet) Scan(ctx 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()) if err != nil { @@ -229,7 +229,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, return nil, errors.New("expecting StatefulSet resource") } switch gvr { - case "v1/configmaps": + case CmGVR: if !hasConfigMap(&ds.Spec.Template.Spec, n) { continue } @@ -237,7 +237,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, GVR: d.GVR(), FQN: client.FQN(ds.Namespace, ds.Name), }) - case "v1/secrets": + case SecGVR: found, err := hasSecret(d.Factory, &ds.Spec.Template.Spec, ds.Namespace, n, wait) if err != nil { log.Warn().Err(err).Msgf("locate secret %q", fqn) @@ -250,7 +250,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, GVR: d.GVR(), FQN: client.FQN(ds.Namespace, ds.Name), }) - case "v1/persistentvolumeclaims": + case PvcGVR: if !hasPVC(&ds.Spec.Template.Spec, n) { continue } @@ -258,7 +258,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, GVR: d.GVR(), FQN: client.FQN(ds.Namespace, ds.Name), }) - case "scheduling.k8s.io/v1/priorityclasses": + case PcGVR: if !hasPC(&ds.Spec.Template.Spec, n) { continue } diff --git a/internal/dao/generic.go b/internal/dao/generic.go index 6d3e7f411d..489c3f835c 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -40,7 +40,7 @@ type Generic struct { func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) { labelSel, _ := ctx.Value(internal.KeyLabels).(string) if client.IsAllNamespace(ns) { - ns = client.AllNamespaces + ns = client.BlankNamespace } var ( diff --git a/internal/dao/job.go b/internal/dao/job.go index 4f70acc700..1444929a34 100644 --- a/internal/dao/job.go +++ b/internal/dao/job.go @@ -133,7 +133,7 @@ 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, fqn string, wait bool) (Refs, error) { +func (j *Job) Scan(ctx 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()) if err != nil { @@ -148,7 +148,7 @@ func (j *Job) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error return nil, errors.New("expecting Job resource") } switch gvr { - case "v1/configmaps": + case CmGVR: if !hasConfigMap(&job.Spec.Template.Spec, n) { continue } @@ -156,7 +156,7 @@ func (j *Job) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error GVR: j.GVR(), FQN: client.FQN(job.Namespace, job.Name), }) - case "v1/secrets": + case SecGVR: found, err := hasSecret(j.Factory, &job.Spec.Template.Spec, job.Namespace, n, wait) if err != nil { log.Warn().Err(err).Msgf("locate secret %q", fqn) @@ -169,7 +169,7 @@ func (j *Job) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error GVR: j.GVR(), FQN: client.FQN(job.Namespace, job.Name), }) - case "scheduling.k8s.io/v1/priorityclasses": + case PcGVR: if !hasPC(&job.Spec.Template.Spec, n) { continue } diff --git a/internal/dao/node.go b/internal/dao/node.go index 6d2984679d..4f8058f4d5 100644 --- a/internal/dao/node.go +++ b/internal/dao/node.go @@ -189,7 +189,7 @@ 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) { var count int - oo, err := n.GetFactory().List("v1/pods", client.AllNamespaces, false, labels.Everything()) + oo, err := n.GetFactory().List("v1/pods", client.BlankNamespace, false, labels.Everything()) if err != nil { return 0, err } @@ -213,7 +213,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.AllNamespaces, false, labels.Everything()) + oo, err := n.GetFactory().List("v1/pods", client.BlankNamespace, false, labels.Everything()) if err != nil { return nil, err } diff --git a/internal/dao/ns.go b/internal/dao/ns.go index 492e3df4de..4daec5f9bc 100644 --- a/internal/dao/ns.go +++ b/internal/dao/ns.go @@ -18,7 +18,7 @@ type Namespace struct { Generic } -// List returns a collection of nodes. +// List returns a collection of namespaces. func (n *Namespace) List(ctx context.Context, ns string) ([]runtime.Object, error) { oo, err := n.Generic.List(ctx, ns) if err != nil { diff --git a/internal/dao/pod.go b/internal/dao/pod.go index 05ef713e6a..d4404f5f0b 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -95,10 +95,10 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) { } var pmx client.PodsMetricsMap - if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok { + if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); false && ok && withMx { pmx, _ = client.DialMetrics(p.Client()).FetchPodsMetricsMap(ctx, ns) } - sel, _ := ctx.Value(internal.KeyFields).(string) + sel, _ := ctx.Value(internal.KeyLabels).(string) fsel, err := labels.ConvertSelectorToLabelsMap(sel) if err != nil { return nil, err @@ -268,7 +268,7 @@ 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, fqn string, wait bool) (Refs, error) { +func (p *Pod) Scan(ctx 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()) if err != nil { @@ -287,7 +287,7 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error continue } switch gvr { - case "v1/configmaps": + case CmGVR: if !hasConfigMap(&pod.Spec, n) { continue } @@ -295,7 +295,7 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error GVR: p.GVR(), FQN: client.FQN(pod.Namespace, pod.Name), }) - case "v1/secrets": + case SecGVR: found, err := hasSecret(p.Factory, &pod.Spec, pod.Namespace, n, wait) if err != nil { log.Warn().Err(err).Msgf("locate secret %q", fqn) @@ -308,7 +308,7 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error GVR: p.GVR(), FQN: client.FQN(pod.Namespace, pod.Name), }) - case "v1/persistentvolumeclaims": + case PvcGVR: if !hasPVC(&pod.Spec, n) { continue } @@ -316,7 +316,7 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error GVR: p.GVR(), FQN: client.FQN(pod.Namespace, pod.Name), }) - case "scheduling.k8s.io/v1/priorityclasses": + case PcGVR: if !hasPC(&pod.Spec, n) { continue } @@ -536,6 +536,8 @@ func (p *Pod) Sanitize(ctx context.Context, ns string) (int, error) { switch render.PodStatus(&pod) { case render.PhaseCompleted: fallthrough + case render.PhasePending: + fallthrough case render.PhaseCrashLoop: fallthrough case render.PhaseError: @@ -543,9 +545,11 @@ func (p *Pod) Sanitize(ctx context.Context, ns string) (int, error) { case render.PhaseImagePullBackOff: fallthrough case render.PhaseOOMKilled: + // !!BOZO!! Might need to bump timeout otherwise rev limit if too many?? log.Debug().Msgf("Sanitizing %s:%s", pod.Namespace, pod.Name) fqn := client.FQN(pod.Namespace, pod.Name) - if err := p.Resource.Delete(ctx, fqn, nil, NowGrace); err != nil { + if err := p.Delete(ctx, fqn, nil, 0); err != nil { + log.Debug().Msgf("Aborted! Sanitizer deleted %d pods", count) return count, err } count++ diff --git a/internal/dao/popeye.go b/internal/dao/popeye.go index 2f50906c74..431065904d 100644 --- a/internal/dao/popeye.go +++ b/internal/dao/popeye.go @@ -68,9 +68,9 @@ func (p *Popeye) List(ctx context.Context, ns string) ([]runtime.Object, error) flags.Sections = §ions flags.ActiveNamespace = &ns } - spinach := cfg.YamlExtension(filepath.Join(cfg.K9sHome(), "spinach.yml")) + spinach := cfg.YamlExtension(filepath.Join(cfg.K9sHome(), "spinach.yaml")) if c, err := p.GetFactory().Client().Config().CurrentContextName(); err == nil { - spinach = cfg.YamlExtension(filepath.Join(cfg.K9sHome(), fmt.Sprintf("%s_spinach.yml", c))) + spinach = cfg.YamlExtension(filepath.Join(cfg.K9sHome(), fmt.Sprintf("%s_spinach.yaml", c))) } if _, err := os.Stat(spinach); err == nil { flags.Spinach = &spinach diff --git a/internal/dao/port_forward.go b/internal/dao/port_forward.go index 3c62ccdc95..4efa7509ef 100644 --- a/internal/dao/port_forward.go +++ b/internal/dao/port_forward.go @@ -38,14 +38,14 @@ func (p *PortForward) Delete(_ context.Context, path string, _ *metav1.DeletionP // List returns a collection of port forwards. func (p *PortForward) List(ctx context.Context, _ string) ([]runtime.Object, error) { benchFile, ok := ctx.Value(internal.KeyBenchCfg).(string) - if !ok { - return nil, fmt.Errorf("no bench file found in context") + if !ok || benchFile == "" { + return nil, fmt.Errorf("no benchmark config file found in context") } path, _ := ctx.Value(internal.KeyPath).(string) config, err := config.NewBench(benchFile) if err != nil { - log.Warn().Msgf("No custom benchmark config file found") + log.Debug().Msgf("No custom benchmark config file found: %q", benchFile) } ff, cc := p.GetFactory().Forwarders(), config.Benchmarks.Containers @@ -92,7 +92,7 @@ func BenchConfigFor(benchFile, path string) config.BenchConfig { def := config.DefaultBenchSpec() cust, err := config.NewBench(benchFile) if err != nil { - log.Debug().Msgf("No custom benchmark config file found") + log.Debug().Msgf("No custom benchmark config file found. Using default: %q", benchFile) return def } if b, ok := cust.Benchmarks.Containers[PodToKey(path)]; ok { diff --git a/internal/dao/port_forward_test.go b/internal/dao/port_forward_test.go index 1fbb0d4721..2ce4de4234 100644 --- a/internal/dao/port_forward_test.go +++ b/internal/dao/port_forward_test.go @@ -17,7 +17,7 @@ func TestBenchForConfig(t *testing.T) { spec config.BenchConfig }{ "no_file": {file: "", key: "", spec: config.DefaultBenchSpec()}, - "spec": {file: "testdata/benchspec.yml", key: "default/nginx-123-456|nginx", spec: config.BenchConfig{ + "spec": {file: "testdata/benchspec.yaml", key: "default/nginx-123-456|nginx", spec: config.BenchConfig{ C: 2, N: 3000, HTTP: config.HTTP{ diff --git a/internal/dao/port_forwarder.go b/internal/dao/port_forwarder.go index 3c6d2928a3..19382e58e4 100644 --- a/internal/dao/port_forwarder.go +++ b/internal/dao/port_forwarder.go @@ -15,7 +15,6 @@ import ( "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -52,8 +51,8 @@ func (p *PortForwarder) String() string { } // Age returns the port forward age. -func (p *PortForwarder) Age() string { - return time.Since(p.age).String() +func (p *PortForwarder) Age() time.Time { + return p.age } // Active returns the forward status. @@ -191,8 +190,8 @@ func codec() (serializer.CodecFactory, runtime.ParameterCodec) { scheme := runtime.NewScheme() gv := schema.GroupVersion{Group: "", Version: "v1"} metav1.AddToGroupVersion(scheme, gv) - scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) - scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) + scheme.AddKnownTypes(gv, &metav1.Table{}, &metav1.TableOptions{}) + scheme.AddKnownTypes(metav1.SchemeGroupVersion, &metav1.Table{}, &metav1.TableOptions{}) return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme) } diff --git a/internal/dao/rbac.go b/internal/dao/rbac.go index b9637d7a79..68dcaddb64 100644 --- a/internal/dao/rbac.go +++ b/internal/dao/rbac.go @@ -36,7 +36,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).(string) + gvr, ok := ctx.Value(internal.KeyGVR).(client.GVR) if !ok { return nil, fmt.Errorf("expecting a context gvr") } @@ -45,8 +45,7 @@ func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) { return r.Resource.List(ctx, ns) } - res := client.NewGVR(gvr) - switch res.R() { + switch gvr.R() { case "clusterrolebindings": return r.loadClusterRoleBinding(path) case "rolebindings": @@ -56,7 +55,7 @@ func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) { case "roles": return r.loadRole(path) default: - return nil, fmt.Errorf("expecting clusterrole/role but found %s", res.R()) + return nil, fmt.Errorf("expecting clusterrole/role but found %s", gvr.R()) } } diff --git a/internal/dao/rbac_policy.go b/internal/dao/rbac_policy.go index 3233c0ba2b..e74f5b7718 100644 --- a/internal/dao/rbac_policy.go +++ b/internal/dao/rbac_policy.go @@ -181,13 +181,13 @@ func (p *Policy) fetchRoleBindingNamespaces(kind, name string) (map[string]strin // isSameSubject verifies if the incoming type name and namespace match a subject from a // cluster/roleBinding. A ServiceAccount will always have a namespace and needs to be validated to ensure // we don't display permissions for a ServiceAccount with the same name in a different namespace -func isSameSubject(kind, namespace, name string, subject *rbacv1.Subject) bool { +func isSameSubject(kind, ns, name string, subject *rbacv1.Subject) bool { if subject.Kind != kind || subject.Name != name { return false } if kind == rbacv1.ServiceAccountKind { // Kind and name were checked above, check the namespace - return subject.Namespace == namespace + return client.IsAllNamespaces(ns) || subject.Namespace == ns } return true } @@ -215,7 +215,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.AllNamespaces, false, labels.Everything()) + oo, err := p.GetFactory().List(gvr, client.BlankNamespace, false, labels.Everything()) if err != nil { return nil, err } diff --git a/internal/dao/reference.go b/internal/dao/reference.go index 25db064a9b..8ea76f5159 100644 --- a/internal/dao/reference.go +++ b/internal/dao/reference.go @@ -22,12 +22,12 @@ 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).(string) + gvr, ok := ctx.Value(internal.KeyGVR).(client.GVR) if !ok { return nil, errors.New("No context GVR found") } switch gvr { - case "v1/serviceaccounts": + case SaGVR: return r.ScanSA(ctx) default: return r.Scan(ctx) diff --git a/internal/dao/registry.go b/internal/dao/registry.go index aee73f22ab..0fb80a0f8e 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -83,6 +83,7 @@ func NewMeta() *Meta { // 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{}, @@ -205,6 +206,14 @@ func loadNonResource(m ResourceMetas) { } func loadK9s(m ResourceMetas) { + m[client.NewGVR("workloads")] = metav1.APIResource{ + Name: "workloads", + Kind: "Workload", + SingularName: "workload", + Namespaced: true, + ShortNames: []string{"wk"}, + Categories: []string{k9sCat}, + } m[client.NewGVR("pulses")] = metav1.APIResource{ Name: "pulses", Kind: "Pulse", diff --git a/internal/dao/rest_mapper.go b/internal/dao/rest_mapper.go index 9e1391d4a9..d9e6f8e3cf 100644 --- a/internal/dao/rest_mapper.go +++ b/internal/dao/rest_mapper.go @@ -28,7 +28,7 @@ func (r *RestMapper) ToRESTMapper() (meta.RESTMapper, error) { return nil, err } mapper := restmapper.NewDeferredDiscoveryRESTMapper(dial) - expander := restmapper.NewShortcutExpander(mapper, dial) + expander := restmapper.NewShortcutExpander(mapper, dial, nil) return expander, nil } diff --git a/internal/dao/sts.go b/internal/dao/sts.go index 6e4b2fc822..3d3dbb4123 100644 --- a/internal/dao/sts.go +++ b/internal/dao/sts.go @@ -215,7 +215,7 @@ 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, fqn string, wait bool) (Refs, error) { +func (s *StatefulSet) Scan(ctx 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()) if err != nil { @@ -230,7 +230,7 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Ref return nil, errors.New("expecting StatefulSet resource") } switch gvr { - case "v1/configmaps": + case CmGVR: if !hasConfigMap(&sts.Spec.Template.Spec, n) { continue } @@ -238,7 +238,7 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Ref GVR: s.GVR(), FQN: client.FQN(sts.Namespace, sts.Name), }) - case "v1/secrets": + case SecGVR: found, err := hasSecret(s.Factory, &sts.Spec.Template.Spec, sts.Namespace, n, wait) if err != nil { log.Warn().Err(err).Msgf("locate secret %q", fqn) @@ -251,7 +251,7 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Ref GVR: s.GVR(), FQN: client.FQN(sts.Namespace, sts.Name), }) - case "v1/persistentvolumeclaims": + case PvcGVR: for _, v := range sts.Spec.VolumeClaimTemplates { if !strings.HasPrefix(n, v.Name+"-"+sts.Name) { continue @@ -268,7 +268,7 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Ref GVR: s.GVR(), FQN: client.FQN(sts.Namespace, sts.Name), }) - case "scheduling.k8s.io/v1/priorityclasses": + case PcGVR: if !hasPC(&sts.Spec.Template.Spec, n) { continue } diff --git a/internal/dao/table.go b/internal/dao/table.go index 823408f9eb..c2569da183 100644 --- a/internal/dao/table.go +++ b/internal/dao/table.go @@ -10,7 +10,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/rest" @@ -25,7 +25,7 @@ type Table struct { // Get returns a given resource. func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { - a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName) + a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName) _, codec := t.codec() c, err := t.getClient() @@ -37,7 +37,7 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { SetHeader("Accept", a). Name(n). Resource(t.gvr.R()). - VersionedParams(&metav1beta1.TableOptions{}, codec) + VersionedParams(&metav1.TableOptions{}, codec) if ns != client.ClusterScope { req = req.Namespace(ns) } @@ -47,12 +47,8 @@ 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, ok := ctx.Value(internal.KeyLabels).(string) - if !ok { - labelSel = "" - } - - a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName) + labelSel, _ := ctx.Value(internal.KeyLabels).(string) + a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName) _, codec := t.codec() c, err := t.getClient() @@ -103,8 +99,8 @@ func (t *Table) codec() (serializer.CodecFactory, runtime.ParameterCodec) { scheme := runtime.NewScheme() gv := t.gvr.GV() metav1.AddToGroupVersion(scheme, gv) - scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) - scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) + scheme.AddKnownTypes(gv, &metav1.Table{}, &metav1.TableOptions{IncludeObject: v1.IncludeObject}) + scheme.AddKnownTypes(metav1.SchemeGroupVersion, &metav1.Table{}, &metav1.TableOptions{IncludeObject: v1.IncludeObject}) return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme) } diff --git a/internal/dao/testdata/benchspec.yml b/internal/dao/testdata/benchspec.yaml similarity index 100% rename from internal/dao/testdata/benchspec.yml rename to internal/dao/testdata/benchspec.yaml diff --git a/internal/dao/testdata/dir/a.yml b/internal/dao/testdata/dir/a.yaml similarity index 100% rename from internal/dao/testdata/dir/a.yml rename to internal/dao/testdata/dir/a.yaml diff --git a/internal/dao/testdata/dir/a/b.yml b/internal/dao/testdata/dir/a/b.yaml similarity index 100% rename from internal/dao/testdata/dir/a/b.yml rename to internal/dao/testdata/dir/a/b.yaml diff --git a/internal/dao/workload.go b/internal/dao/workload.go new file mode 100644 index 0000000000..ae9693b831 --- /dev/null +++ b/internal/dao/workload.go @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dao + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +const ( + StatusOK = "OK" + 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} +) + +// Workload tracks a select set of resources in a given namespace. +type Workload struct { + Table +} + +func (w *Workload) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error { + gvr, _ := ctx.Value(internal.KeyGVR).(client.GVR) + ns, n := client.Namespaced(path) + auth, err := w.Client().CanI(ns, gvr.String(), []string{client.DeleteVerb}) + if err != nil { + return err + } + if !auth { + return fmt.Errorf("user is not authorized to delete %s", path) + } + + var gracePeriod *int64 + if grace != DefaultGrace { + gracePeriod = (*int64)(&grace) + } + opts := metav1.DeleteOptions{ + PropagationPolicy: propagation, + GracePeriodSeconds: gracePeriod, + } + + ctx, cancel := context.WithTimeout(ctx, w.Client().Config().CallTimeout()) + defer cancel() + + d, err := w.Client().DynDial() + if err != nil { + return err + } + dial := d.Resource(gvr.GVR()) + if client.IsClusterScoped(ns) { + return dial.Delete(ctx, n, opts) + } + + return dial.Namespace(ns).Delete(ctx, n, opts) +} + +func (a *Workload) fetch(ctx context.Context, gvr client.GVR, ns string) (*metav1.Table, error) { + a.Table.gvr = gvr + oo, err := a.Table.List(ctx, ns) + if err != nil { + return nil, err + } + if len(oo) == 0 { + return nil, fmt.Errorf("no table found for gvr: %s", gvr) + } + tt, ok := oo[0].(*metav1.Table) + if !ok { + return nil, errors.New("not a metav1.Table") + } + + return tt, nil +} + +// List fetch workloads. +func (a *Workload) List(ctx context.Context, ns string) ([]runtime.Object, error) { + oo := make([]runtime.Object, 0, 100) + for _, gvr := range resList { + table, err := a.fetch(ctx, gvr, ns) + if err != nil { + return nil, err + } + var ( + ns string + ts metav1.Time + ) + for _, r := range table.Rows { + if obj := r.Object.Object; obj != nil { + if m, err := meta.Accessor(obj); err == nil { + ns = m.GetNamespace() + ts = m.GetCreationTimestamp() + } + } else { + var m metav1.PartialObjectMetadata + if err := json.Unmarshal(r.Object.Raw, &m); err == nil { + ns = m.GetNamespace() + ts = m.CreationTimestamp + } + } + oo = append(oo, &render.WorkloadRes{Row: metav1.TableRow{Cells: []interface{}{ + gvr.String(), + ns, + r.Cells[indexOf("Name", table.ColumnDefinitions)], + diagnose(gvr, r, table.ColumnDefinitions), + status(gvr, r, table.ColumnDefinitions), + ts, + }}}) + } + } + + return oo, nil +} + +// Helpers... + +func status(gvr client.GVR, r metav1.TableRow, h []metav1.TableColumnDefinition) string { + switch gvr { + case PodGVR, DpGVR, StsGVR: + return r.Cells[indexOf("Ready", h)].(string) + case RsGVR, DsGVR: + c := r.Cells[indexOf("Ready", h)].(int64) + d := r.Cells[indexOf("Desired", h)].(int64) + return fmt.Sprintf("%d/%d", c, d) + case SvcGVR: + return "" + } + + return render.NAValue +} + +func diagnose(gvr client.GVR, r metav1.TableRow, h []metav1.TableColumnDefinition) string { + switch gvr { + case PodGVR: + if !isReady(r.Cells[indexOf("Ready", h)].(string)) || r.Cells[indexOf("Status", h)] != render.PhaseRunning { + return DegradedStatus + } + case DpGVR, StsGVR: + if !isReady(r.Cells[indexOf("Ready", h)].(string)) { + return DegradedStatus + } + case RsGVR, DsGVR: + rd, ok1 := r.Cells[indexOf("Ready", h)].(int64) + de, ok2 := r.Cells[indexOf("Desired", h)].(int64) + if ok1 && ok2 { + if !isReady(fmt.Sprintf("%d/%d", rd, de)) { + return DegradedStatus + } + break + } + rds, oks1 := r.Cells[indexOf("Ready", h)].(string) + des, oks2 := r.Cells[indexOf("Desired", h)].(string) + if oks1 && oks2 { + if !isReady(fmt.Sprintf("%s/%s", rds, des)) { + return DegradedStatus + } + } + case SvcGVR: + default: + return render.MissingValue + } + + return StatusOK +} + +func isReady(s string) bool { + tt := strings.Split(s, "/") + if len(tt) != 2 { + return false + } + r, err := strconv.Atoi(tt[0]) + if err != nil { + log.Error().Msgf("invalid ready count: %q", tt[0]) + return false + } + c, err := strconv.Atoi(tt[1]) + if err != nil { + log.Error().Msgf("invalid expected count: %q", tt[1]) + return false + } + + if c == 0 { + return true + } + return r == c +} + +func indexOf(n string, defs []metav1.TableColumnDefinition) int { + for i, d := range defs { + if d.Name == n { + return i + } + } + + return -1 +} diff --git a/internal/model/cluster.go b/internal/model/cluster.go index d4d2ce30ec..3182a0e13c 100644 --- a/internal/model/cluster.go +++ b/internal/model/cluster.go @@ -58,7 +58,7 @@ func NewCluster(f dao.Factory) *Cluster { // Version returns the current K8s cluster version. func (c *Cluster) Version() string { info, err := c.factory.Client().ServerVersion() - if err != nil { + if err != nil || info == nil { return client.NA } @@ -74,7 +74,7 @@ func (c *Cluster) ContextName() string { return n } -// ClusterName returns the cluster name. +// ClusterName returns the context name. func (c *Cluster) ClusterName() string { n, err := c.factory.Client().Config().CurrentClusterName() if err != nil { diff --git a/internal/model/fish_buff.go b/internal/model/fish_buff.go index b21e2f497b..1bfb0ae003 100644 --- a/internal/model/fish_buff.go +++ b/internal/model/fish_buff.go @@ -136,5 +136,4 @@ func (f *FishBuff) fireSuggestionChanged(ss []string) { suggest = ss[f.suggestionIndex] } f.SetText(f.GetText(), suggest) - } diff --git a/internal/model/helpers_int_test.go b/internal/model/helpers_int_test.go index 3a21a4d2b4..5c3f39a911 100644 --- a/internal/model/helpers_int_test.go +++ b/internal/model/helpers_int_test.go @@ -4,9 +4,10 @@ package model import ( + "testing" + "github.com/sahilm/fuzzy" "github.com/stretchr/testify/assert" - "testing" ) func Test_rxFilter(t *testing.T) { diff --git a/internal/model/history.go b/internal/model/history.go index 04881796f5..7469e32af6 100644 --- a/internal/model/history.go +++ b/internal/model/history.go @@ -5,8 +5,6 @@ package model import ( "strings" - - "github.com/rs/zerolog/log" ) // MaxHistory tracks max command history. @@ -25,6 +23,14 @@ func NewHistory(limit int) *History { } } +func (h *History) Pop() string { + if h.Empty() { + return "" + } + + return h.commands[0] +} + // List returns the current command history. func (h *History) List() []string { return h.commands @@ -49,7 +55,6 @@ func (h *History) Push(c string) { // Clear clears out the stack. func (h *History) Clear() { - log.Debug().Msgf("History CLEARED!!!") h.commands = nil } diff --git a/internal/model/mock_clustermeta_test.go b/internal/model/mock_clustermeta_test.go deleted file mode 100644 index bec8892605..0000000000 --- a/internal/model/mock_clustermeta_test.go +++ /dev/null @@ -1,869 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/model (interfaces: ClusterMeta) - -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package model_test - -import ( - "reflect" - "time" - - client "github.com/derailed/k9s/internal/client" - pegomock "github.com/petergtz/pegomock" - v1 "k8s.io/api/core/v1" - version "k8s.io/apimachinery/pkg/version" - disk "k8s.io/client-go/discovery/cached/disk" - dynamic "k8s.io/client-go/dynamic" - kubernetes "k8s.io/client-go/kubernetes" - rest "k8s.io/client-go/rest" - versioned "k8s.io/metrics/pkg/client/clientset/versioned" -) - -type MockClusterMeta struct { - fail func(message string, callerSkip ...int) -} - -func NewMockClusterMeta(options ...pegomock.Option) *MockClusterMeta { - mock := &MockClusterMeta{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockClusterMeta) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockClusterMeta) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockClusterMeta) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CachedDiscovery", params, []reflect.Type{reflect.TypeOf((**disk.CachedDiscoveryClient)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *disk.CachedDiscoveryClient - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*disk.CachedDiscoveryClient) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) CanI(_param0 string, _param1 string, _param2 []string) (bool, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{_param0, _param1, _param2} - result := pegomock.GetGenericMockFrom(mock).Invoke("CanI", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 bool - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) ClusterName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ClusterName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) Config() *client.Config { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("Config", params, []reflect.Type{reflect.TypeOf((**client.Config)(nil)).Elem()}) - var ret0 *client.Config - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*client.Config) - } - } - return ret0 -} - -func (mock *MockClusterMeta) ContextName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ContextName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) CurrentNamespaceName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CurrentNamespaceName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) DialOrDie() kubernetes.Interface { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("DialOrDie", params, []reflect.Type{reflect.TypeOf((*kubernetes.Interface)(nil)).Elem()}) - var ret0 kubernetes.Interface - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(kubernetes.Interface) - } - } - return ret0 -} - -func (mock *MockClusterMeta) DynDialOrDie() dynamic.Interface { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("DynDialOrDie", params, []reflect.Type{reflect.TypeOf((*dynamic.Interface)(nil)).Elem()}) - var ret0 dynamic.Interface - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(dynamic.Interface) - } - } - return ret0 -} - -func (mock *MockClusterMeta) GetNodes() (*v1.NodeList, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("GetNodes", params, []reflect.Type{reflect.TypeOf((**v1.NodeList)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *v1.NodeList - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*v1.NodeList) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) HasMetrics() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("HasMetrics", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockClusterMeta) IsNamespaced(_param0 string) bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("IsNamespaced", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockClusterMeta) MXDial() (*versioned.Clientset, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("MXDial", params, []reflect.Type{reflect.TypeOf((**versioned.Clientset)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *versioned.Clientset - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*versioned.Clientset) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) NodePods(_param0 string) (*v1.PodList, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("NodePods", params, []reflect.Type{reflect.TypeOf((**v1.PodList)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *v1.PodList - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*v1.PodList) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) RestConfigOrDie() *rest.Config { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("RestConfigOrDie", params, []reflect.Type{reflect.TypeOf((**rest.Config)(nil)).Elem()}) - var ret0 *rest.Config - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*rest.Config) - } - } - return ret0 -} - -func (mock *MockClusterMeta) ServerVersion() (*version.Info, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ServerVersion", params, []reflect.Type{reflect.TypeOf((**version.Info)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *version.Info - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*version.Info) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (string, bool, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{_param0, _param1} - result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 bool - var ret2 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(bool) - } - if result[2] != nil { - ret2 = result[2].(error) - } - } - return ret0, ret1, ret2 -} - -func (mock *MockClusterMeta) SupportsResource(_param0 string) bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsResource", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockClusterMeta) SwitchContext(_param0 string) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{_param0} - pegomock.GetGenericMockFrom(mock).Invoke("SwitchContext", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - - return nil -} - -func (mock *MockClusterMeta) UserName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("UserName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) ValidNamespaces() ([]v1.Namespace, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ValidNamespaces", params, []reflect.Type{reflect.TypeOf((*[]v1.Namespace)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 []v1.Namespace - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].([]v1.Namespace) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) Version() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("Version", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) VerifyWasCalledOnce() *VerifierMockClusterMeta { - return &VerifierMockClusterMeta{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockClusterMeta) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockClusterMeta { - return &VerifierMockClusterMeta{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockClusterMeta) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockClusterMeta { - return &VerifierMockClusterMeta{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockClusterMeta) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockClusterMeta { - return &VerifierMockClusterMeta{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockClusterMeta struct { - mock *MockClusterMeta - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockClusterMeta) CachedDiscovery() *MockClusterMeta_CachedDiscovery_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CachedDiscovery", params, verifier.timeout) - return &MockClusterMeta_CachedDiscovery_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_CachedDiscovery_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_CachedDiscovery_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_CachedDiscovery_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) CanI(_param0 string, _param1 string, _param2 []string) *MockClusterMeta_CanI_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CanI", params, verifier.timeout) - return &MockClusterMeta_CanI_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_CanI_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_CanI_OngoingVerification) GetCapturedArguments() (string, string, []string) { - _param0, _param1, _param2 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1] -} - -func (c *MockClusterMeta_CanI_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 [][]string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - _param1 = make([]string, len(params[1])) - for u, param := range params[1] { - _param1[u] = param.(string) - } - _param2 = make([][]string, len(params[2])) - for u, param := range params[2] { - _param2[u] = param.([]string) - } - } - return -} - -func (verifier *VerifierMockClusterMeta) ClusterName() *MockClusterMeta_ClusterName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ClusterName", params, verifier.timeout) - return &MockClusterMeta_ClusterName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_ClusterName_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_ClusterName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_ClusterName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) Config() *MockClusterMeta_Config_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Config", params, verifier.timeout) - return &MockClusterMeta_Config_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_Config_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_Config_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_Config_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) ContextName() *MockClusterMeta_ContextName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ContextName", params, verifier.timeout) - return &MockClusterMeta_ContextName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_ContextName_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_ContextName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_ContextName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) CurrentNamespaceName() *MockClusterMeta_CurrentNamespaceName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CurrentNamespaceName", params, verifier.timeout) - return &MockClusterMeta_CurrentNamespaceName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_CurrentNamespaceName_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_CurrentNamespaceName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_CurrentNamespaceName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) DialOrDie() *MockClusterMeta_DialOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DialOrDie", params, verifier.timeout) - return &MockClusterMeta_DialOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_DialOrDie_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_DialOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_DialOrDie_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) DynDialOrDie() *MockClusterMeta_DynDialOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DynDialOrDie", params, verifier.timeout) - return &MockClusterMeta_DynDialOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_DynDialOrDie_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_DynDialOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_DynDialOrDie_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) GetNodes() *MockClusterMeta_GetNodes_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetNodes", params, verifier.timeout) - return &MockClusterMeta_GetNodes_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_GetNodes_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_GetNodes_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_GetNodes_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) HasMetrics() *MockClusterMeta_HasMetrics_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasMetrics", params, verifier.timeout) - return &MockClusterMeta_HasMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_HasMetrics_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_HasMetrics_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_HasMetrics_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) IsNamespaced(_param0 string) *MockClusterMeta_IsNamespaced_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "IsNamespaced", params, verifier.timeout) - return &MockClusterMeta_IsNamespaced_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_IsNamespaced_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_IsNamespaced_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockClusterMeta_IsNamespaced_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockClusterMeta) MXDial() *MockClusterMeta_MXDial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "MXDial", params, verifier.timeout) - return &MockClusterMeta_MXDial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_MXDial_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_MXDial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_MXDial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) NodePods(_param0 string) *MockClusterMeta_NodePods_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "NodePods", params, verifier.timeout) - return &MockClusterMeta_NodePods_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_NodePods_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_NodePods_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockClusterMeta_NodePods_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockClusterMeta) RestConfigOrDie() *MockClusterMeta_RestConfigOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RestConfigOrDie", params, verifier.timeout) - return &MockClusterMeta_RestConfigOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_RestConfigOrDie_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_RestConfigOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_RestConfigOrDie_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) ServerVersion() *MockClusterMeta_ServerVersion_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ServerVersion", params, verifier.timeout) - return &MockClusterMeta_ServerVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_ServerVersion_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_ServerVersion_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_ServerVersion_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) SupportsRes(_param0 string, _param1 []string) *MockClusterMeta_SupportsRes_OngoingVerification { - params := []pegomock.Param{_param0, _param1} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsRes", params, verifier.timeout) - return &MockClusterMeta_SupportsRes_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_SupportsRes_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_SupportsRes_OngoingVerification) GetCapturedArguments() (string, []string) { - _param0, _param1 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1] -} - -func (c *MockClusterMeta_SupportsRes_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 [][]string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - _param1 = make([][]string, len(params[1])) - for u, param := range params[1] { - _param1[u] = param.([]string) - } - } - return -} - -func (verifier *VerifierMockClusterMeta) SupportsResource(_param0 string) *MockClusterMeta_SupportsResource_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsResource", params, verifier.timeout) - return &MockClusterMeta_SupportsResource_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_SupportsResource_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_SupportsResource_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockClusterMeta_SupportsResource_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockClusterMeta) SwitchContextOrDie(_param0 string) *MockClusterMeta_SwitchContextOrDie_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SwitchContextOrDie", params, verifier.timeout) - return &MockClusterMeta_SwitchContextOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_SwitchContextOrDie_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_SwitchContextOrDie_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockClusterMeta_SwitchContextOrDie_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockClusterMeta) UserName() *MockClusterMeta_UserName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "UserName", params, verifier.timeout) - return &MockClusterMeta_UserName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_UserName_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_UserName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_UserName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) ValidNamespaces() *MockClusterMeta_ValidNamespaces_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ValidNamespaces", params, verifier.timeout) - return &MockClusterMeta_ValidNamespaces_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_ValidNamespaces_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_ValidNamespaces_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_ValidNamespaces_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) Version() *MockClusterMeta_Version_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Version", params, verifier.timeout) - return &MockClusterMeta_Version_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_Version_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_Version_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_Version_OngoingVerification) GetAllCapturedArguments() { -} diff --git a/internal/model/mock_connection_test.go b/internal/model/mock_connection_test.go deleted file mode 100644 index e0ed4c9a60..0000000000 --- a/internal/model/mock_connection_test.go +++ /dev/null @@ -1,655 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/client (interfaces: Connection) - -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package model_test - -import ( - client "github.com/derailed/k9s/internal/client" - pegomock "github.com/petergtz/pegomock" - v1 "k8s.io/api/core/v1" - version "k8s.io/apimachinery/pkg/version" - disk "k8s.io/client-go/discovery/cached/disk" - dynamic "k8s.io/client-go/dynamic" - kubernetes "k8s.io/client-go/kubernetes" - rest "k8s.io/client-go/rest" - versioned "k8s.io/metrics/pkg/client/clientset/versioned" - "reflect" - "time" -) - -type MockConnection struct { - fail func(message string, callerSkip ...int) -} - -func NewMockConnection(options ...pegomock.Option) *MockConnection { - mock := &MockConnection{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockConnection) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockConnection) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockConnection) ActiveCluster() string { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ActiveCluster", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()}) - var ret0 string - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - } - return ret0 -} - -func (mock *MockConnection) ActiveNamespace() string { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ActiveNamespace", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()}) - var ret0 string - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - } - return ret0 -} - -func (mock *MockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CachedDiscovery", params, []reflect.Type{reflect.TypeOf((**disk.CachedDiscoveryClient)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *disk.CachedDiscoveryClient - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*disk.CachedDiscoveryClient) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) CanI(_param0 string, _param1 string, _param2 []string) (bool, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0, _param1, _param2} - result := pegomock.GetGenericMockFrom(mock).Invoke("CanI", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 bool - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) CheckConnectivity() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CheckConnectivity", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) Config() *client.Config { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("Config", params, []reflect.Type{reflect.TypeOf((**client.Config)(nil)).Elem()}) - var ret0 *client.Config - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*client.Config) - } - } - return ret0 -} - -func (mock *MockConnection) ConnectionOK() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ConnectionOK", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) Dial() (kubernetes.Interface, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("Dial", params, []reflect.Type{reflect.TypeOf((*kubernetes.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 kubernetes.Interface - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(kubernetes.Interface) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) DynDial() (dynamic.Interface, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("DynDial", params, []reflect.Type{reflect.TypeOf((*dynamic.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 dynamic.Interface - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(dynamic.Interface) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) HasMetrics() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("HasMetrics", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) IsActiveNamespace(_param0 string) bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("IsActiveNamespace", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) MXDial() (*versioned.Clientset, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("MXDial", params, []reflect.Type{reflect.TypeOf((**versioned.Clientset)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *versioned.Clientset - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*versioned.Clientset) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) RestConfig() (*rest.Config, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("RestConfig", params, []reflect.Type{reflect.TypeOf((**rest.Config)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *rest.Config - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*rest.Config) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) ServerVersion() (*version.Info, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ServerVersion", params, []reflect.Type{reflect.TypeOf((**version.Info)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *version.Info - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*version.Info) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) SwitchContext(_param0 string) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("SwitchContext", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(error) - } - } - return ret0 -} - -func (mock *MockConnection) ValidNamespaces() ([]v1.Namespace, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ValidNamespaces", params, []reflect.Type{reflect.TypeOf((*[]v1.Namespace)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 []v1.Namespace - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].([]v1.Namespace) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) VerifyWasCalledOnce() *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockConnection) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockConnection) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockConnection) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockConnection struct { - mock *MockConnection - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockConnection) ActiveCluster() *MockConnection_ActiveCluster_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ActiveCluster", params, verifier.timeout) - return &MockConnection_ActiveCluster_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ActiveCluster_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ActiveCluster_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ActiveCluster_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) ActiveNamespace() *MockConnection_ActiveNamespace_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ActiveNamespace", params, verifier.timeout) - return &MockConnection_ActiveNamespace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ActiveNamespace_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ActiveNamespace_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ActiveNamespace_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CachedDiscovery() *MockConnection_CachedDiscovery_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CachedDiscovery", params, verifier.timeout) - return &MockConnection_CachedDiscovery_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CachedDiscovery_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CachedDiscovery_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CachedDiscovery_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CanI(_param0 string, _param1 string, _param2 []string) *MockConnection_CanI_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CanI", params, verifier.timeout) - return &MockConnection_CanI_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CanI_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CanI_OngoingVerification) GetCapturedArguments() (string, string, []string) { - _param0, _param1, _param2 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1] -} - -func (c *MockConnection_CanI_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 [][]string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - _param1 = make([]string, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(string) - } - _param2 = make([][]string, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.([]string) - } - } - return -} - -func (verifier *VerifierMockConnection) CheckConnectivity() *MockConnection_CheckConnectivity_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckConnectivity", params, verifier.timeout) - return &MockConnection_CheckConnectivity_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CheckConnectivity_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CheckConnectivity_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CheckConnectivity_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) Config() *MockConnection_Config_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Config", params, verifier.timeout) - return &MockConnection_Config_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_Config_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_Config_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_Config_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) ConnectionOK() *MockConnection_ConnectionOK_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ConnectionOK", params, verifier.timeout) - return &MockConnection_ConnectionOK_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ConnectionOK_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ConnectionOK_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ConnectionOK_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) Dial() *MockConnection_Dial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Dial", params, verifier.timeout) - return &MockConnection_Dial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_Dial_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_Dial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_Dial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) DynDial() *MockConnection_DynDial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DynDial", params, verifier.timeout) - return &MockConnection_DynDial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_DynDial_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_DynDial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_DynDial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) HasMetrics() *MockConnection_HasMetrics_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasMetrics", params, verifier.timeout) - return &MockConnection_HasMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_HasMetrics_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_HasMetrics_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_HasMetrics_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) IsActiveNamespace(_param0 string) *MockConnection_IsActiveNamespace_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "IsActiveNamespace", params, verifier.timeout) - return &MockConnection_IsActiveNamespace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_IsActiveNamespace_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_IsActiveNamespace_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_IsActiveNamespace_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockConnection) MXDial() *MockConnection_MXDial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "MXDial", params, verifier.timeout) - return &MockConnection_MXDial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_MXDial_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_MXDial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_MXDial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) RestConfig() *MockConnection_RestConfig_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RestConfig", params, verifier.timeout) - return &MockConnection_RestConfig_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_RestConfig_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_RestConfig_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_RestConfig_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) ServerVersion() *MockConnection_ServerVersion_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ServerVersion", params, verifier.timeout) - return &MockConnection_ServerVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ServerVersion_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ServerVersion_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ServerVersion_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) SwitchContext(_param0 string) *MockConnection_SwitchContext_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SwitchContext", params, verifier.timeout) - return &MockConnection_SwitchContext_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_SwitchContext_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_SwitchContext_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_SwitchContext_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockConnection) ValidNamespaces() *MockConnection_ValidNamespaces_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ValidNamespaces", params, verifier.timeout) - return &MockConnection_ValidNamespaces_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ValidNamespaces_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ValidNamespaces_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ValidNamespaces_OngoingVerification) GetAllCapturedArguments() { -} diff --git a/internal/model/mock_metricsserver_test.go b/internal/model/mock_metricsserver_test.go deleted file mode 100644 index f206c11b4b..0000000000 --- a/internal/model/mock_metricsserver_test.go +++ /dev/null @@ -1,314 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/model (interfaces: MetricsServer) - -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package model_test - -import ( - client "github.com/derailed/k9s/internal/client" - pegomock "github.com/petergtz/pegomock" - v1 "k8s.io/api/core/v1" - v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" - "reflect" - "time" -) - -type MockMetricsServer struct { - fail func(message string, callerSkip ...int) -} - -func NewMockMetricsServer(options ...pegomock.Option) *MockMetricsServer { - mock := &MockMetricsServer{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockMetricsServer) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockMetricsServer) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockMetricsServer) ClusterLoad(_param0 *v1.NodeList, _param1 *v1beta1.NodeMetricsList, _param2 *client.ClusterMetrics) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockMetricsServer().") - } - params := []pegomock.Param{_param0, _param1, _param2} - result := pegomock.GetGenericMockFrom(mock).Invoke("ClusterLoad", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(error) - } - } - return ret0 -} - -func (mock *MockMetricsServer) FetchNodesMetrics() (*v1beta1.NodeMetricsList, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockMetricsServer().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("FetchNodesMetrics", params, []reflect.Type{reflect.TypeOf((**v1beta1.NodeMetricsList)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *v1beta1.NodeMetricsList - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*v1beta1.NodeMetricsList) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockMetricsServer) FetchPodsMetrics(_param0 string) (*v1beta1.PodMetricsList, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockMetricsServer().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("FetchPodsMetrics", params, []reflect.Type{reflect.TypeOf((**v1beta1.PodMetricsList)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *v1beta1.PodMetricsList - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*v1beta1.PodMetricsList) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockMetricsServer) HasMetrics() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockMetricsServer().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("HasMetrics", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockMetricsServer) NodesMetrics(_param0 *v1.NodeList, _param1 *v1beta1.NodeMetricsList, _param2 client.NodesMetrics) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockMetricsServer().") - } - params := []pegomock.Param{_param0, _param1, _param2} - pegomock.GetGenericMockFrom(mock).Invoke("NodesMetrics", params, []reflect.Type{}) -} - -func (mock *MockMetricsServer) PodsMetrics(_param0 *v1beta1.PodMetricsList, _param1 client.PodsMetrics) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockMetricsServer().") - } - params := []pegomock.Param{_param0, _param1} - pegomock.GetGenericMockFrom(mock).Invoke("PodsMetrics", params, []reflect.Type{}) -} - -func (mock *MockMetricsServer) VerifyWasCalledOnce() *VerifierMockMetricsServer { - return &VerifierMockMetricsServer{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockMetricsServer) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockMetricsServer { - return &VerifierMockMetricsServer{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockMetricsServer) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockMetricsServer { - return &VerifierMockMetricsServer{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockMetricsServer) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockMetricsServer { - return &VerifierMockMetricsServer{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockMetricsServer struct { - mock *MockMetricsServer - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockMetricsServer) ClusterLoad(_param0 *v1.NodeList, _param1 *v1beta1.NodeMetricsList, _param2 *client.ClusterMetrics) *MockMetricsServer_ClusterLoad_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ClusterLoad", params, verifier.timeout) - return &MockMetricsServer_ClusterLoad_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockMetricsServer_ClusterLoad_OngoingVerification struct { - mock *MockMetricsServer - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockMetricsServer_ClusterLoad_OngoingVerification) GetCapturedArguments() (*v1.NodeList, *v1beta1.NodeMetricsList, *client.ClusterMetrics) { - _param0, _param1, _param2 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1] -} - -func (c *MockMetricsServer_ClusterLoad_OngoingVerification) GetAllCapturedArguments() (_param0 []*v1.NodeList, _param1 []*v1beta1.NodeMetricsList, _param2 []*client.ClusterMetrics) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]*v1.NodeList, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(*v1.NodeList) - } - _param1 = make([]*v1beta1.NodeMetricsList, len(params[1])) - for u, param := range params[1] { - _param1[u] = param.(*v1beta1.NodeMetricsList) - } - _param2 = make([]*client.ClusterMetrics, len(params[2])) - for u, param := range params[2] { - _param2[u] = param.(*client.ClusterMetrics) - } - } - return -} - -func (verifier *VerifierMockMetricsServer) FetchNodesMetrics() *MockMetricsServer_FetchNodesMetrics_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "FetchNodesMetrics", params, verifier.timeout) - return &MockMetricsServer_FetchNodesMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockMetricsServer_FetchNodesMetrics_OngoingVerification struct { - mock *MockMetricsServer - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockMetricsServer_FetchNodesMetrics_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockMetricsServer_FetchNodesMetrics_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockMetricsServer) FetchPodsMetrics(_param0 string) *MockMetricsServer_FetchPodsMetrics_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "FetchPodsMetrics", params, verifier.timeout) - return &MockMetricsServer_FetchPodsMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockMetricsServer_FetchPodsMetrics_OngoingVerification struct { - mock *MockMetricsServer - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockMetricsServer_FetchPodsMetrics_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockMetricsServer_FetchPodsMetrics_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockMetricsServer) HasMetrics() *MockMetricsServer_HasMetrics_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasMetrics", params, verifier.timeout) - return &MockMetricsServer_HasMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockMetricsServer_HasMetrics_OngoingVerification struct { - mock *MockMetricsServer - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockMetricsServer_HasMetrics_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockMetricsServer_HasMetrics_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockMetricsServer) NodesMetrics(_param0 *v1.NodeList, _param1 *v1beta1.NodeMetricsList, _param2 client.NodesMetrics) *MockMetricsServer_NodesMetrics_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "NodesMetrics", params, verifier.timeout) - return &MockMetricsServer_NodesMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockMetricsServer_NodesMetrics_OngoingVerification struct { - mock *MockMetricsServer - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockMetricsServer_NodesMetrics_OngoingVerification) GetCapturedArguments() (*v1.NodeList, *v1beta1.NodeMetricsList, client.NodesMetrics) { - _param0, _param1, _param2 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1] -} - -func (c *MockMetricsServer_NodesMetrics_OngoingVerification) GetAllCapturedArguments() (_param0 []*v1.NodeList, _param1 []*v1beta1.NodeMetricsList, _param2 []client.NodesMetrics) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]*v1.NodeList, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(*v1.NodeList) - } - _param1 = make([]*v1beta1.NodeMetricsList, len(params[1])) - for u, param := range params[1] { - _param1[u] = param.(*v1beta1.NodeMetricsList) - } - _param2 = make([]client.NodesMetrics, len(params[2])) - for u, param := range params[2] { - _param2[u] = param.(client.NodesMetrics) - } - } - return -} - -func (verifier *VerifierMockMetricsServer) PodsMetrics(_param0 *v1beta1.PodMetricsList, _param1 client.PodsMetrics) *MockMetricsServer_PodsMetrics_OngoingVerification { - params := []pegomock.Param{_param0, _param1} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "PodsMetrics", params, verifier.timeout) - return &MockMetricsServer_PodsMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockMetricsServer_PodsMetrics_OngoingVerification struct { - mock *MockMetricsServer - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockMetricsServer_PodsMetrics_OngoingVerification) GetCapturedArguments() (*v1beta1.PodMetricsList, client.PodsMetrics) { - _param0, _param1 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1] -} - -func (c *MockMetricsServer_PodsMetrics_OngoingVerification) GetAllCapturedArguments() (_param0 []*v1beta1.PodMetricsList, _param1 []client.PodsMetrics) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]*v1beta1.PodMetricsList, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(*v1beta1.PodMetricsList) - } - _param1 = make([]client.PodsMetrics, len(params[1])) - for u, param := range params[1] { - _param1[u] = param.(client.PodsMetrics) - } - } - return -} diff --git a/internal/model/pulse_health.go b/internal/model/pulse_health.go index ecac480c9a..3cf71c23de 100644 --- a/internal/model/pulse_health.go +++ b/internal/model/pulse_health.go @@ -12,7 +12,7 @@ import ( "github.com/derailed/k9s/internal/health" "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -119,7 +119,7 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check, c := health.NewCheck(gvr) if meta.Renderer.IsGeneric() { - table, ok := oo[0].(*metav1beta1.Table) + table, ok := oo[0].(*metav1.Table) if !ok { return nil, fmt.Errorf("expecting a meta table but got %T", oo[0]) } diff --git a/internal/model/registry.go b/internal/model/registry.go index 8b7e238116..9c333f3106 100644 --- a/internal/model/registry.go +++ b/internal/model/registry.go @@ -13,6 +13,10 @@ 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{}, diff --git a/internal/model/rev_values.go b/internal/model/rev_values.go new file mode 100644 index 0000000000..e25ef7b410 --- /dev/null +++ b/internal/model/rev_values.go @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model + +import ( + "context" + "strings" + "sync/atomic" + "time" + + backoff "github.com/cenkalti/backoff/v4" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/rs/zerolog/log" + "github.com/sahilm/fuzzy" +) + +// RevValues tracks Helm values representations. +type RevValues struct { + gvr client.GVR + inUpdate int32 + path string + rev string + query string + lines []string + allValues bool + listeners []ResourceViewerListener + options ViewerToggleOpts +} + +// NewRevValues return a new Helm values resource model. +func NewRevValues(gvr client.GVR, path, rev string) *RevValues { + return &RevValues{ + gvr: gvr, + path: path, + rev: rev, + allValues: false, + lines: getRevValues(path, rev), + } +} + +func getHelmHistDao() *dao.HelmHistory { + return Registry["helm-history"].DAO.(*dao.HelmHistory) +} + +func getRevValues(path, rev string) []string { + vals, err := getHelmHistDao().GetValues(path, true) + if err != nil { + log.Error().Err(err).Msgf("Failed to get Helm values") + } + return strings.Split(string(vals), "\n") +} + +// GVR returns the resource gvr. +func (v *RevValues) GVR() client.GVR { + return v.gvr +} + +// GetPath returns the active resource path. +func (v *RevValues) GetPath() string { + return v.path +} + +// SetOptions toggle model options. +func (v *RevValues) SetOptions(ctx context.Context, opts ViewerToggleOpts) { + v.options = opts + if err := v.refresh(ctx); err != nil { + v.fireResourceFailed(err) + } +} + +// Filter filters the model. +func (v *RevValues) Filter(q string) { + v.query = q + v.filterChanged(v.lines) +} + +func (v *RevValues) filterChanged(lines []string) { + v.fireResourceChanged(lines, v.filter(v.query, lines)) +} + +func (v *RevValues) filter(q string, lines []string) fuzzy.Matches { + if q == "" { + return nil + } + if dao.IsFuzzySelector(q) { + return v.fuzzyFilter(strings.TrimSpace(q[2:]), lines) + } + return rxFilter(q, lines) +} + +func (*RevValues) fuzzyFilter(q string, lines []string) fuzzy.Matches { + return fuzzy.Find(q, lines) +} + +func (v *RevValues) fireResourceChanged(lines []string, matches fuzzy.Matches) { + for _, l := range v.listeners { + l.ResourceChanged(lines, matches) + } +} + +func (v *RevValues) fireResourceFailed(err error) { + for _, l := range v.listeners { + l.ResourceFailed(err) + } +} + +// ClearFilter clear out the filter. +func (v *RevValues) ClearFilter() { + v.query = "" +} + +// Peek returns the current model data. +func (v *RevValues) Peek() []string { + return v.lines +} + +// Refresh updates model data. +func (v *RevValues) Refresh(ctx context.Context) error { + return v.refresh(ctx) +} + +// Watch watches for Values changes. +func (v *RevValues) Watch(ctx context.Context) error { + if err := v.refresh(ctx); err != nil { + return err + } + go v.updater(ctx) + + return nil +} + +func (v *RevValues) updater(ctx context.Context) { + defer log.Debug().Msgf("YAML canceled -- %q", v.gvr) + + backOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval) + delay := defaultReaderRefreshRate + for { + select { + case <-ctx.Done(): + return + case <-time.After(delay): + if err := v.refresh(ctx); err != nil { + v.fireResourceFailed(err) + if delay = backOff.NextBackOff(); delay == backoff.Stop { + log.Error().Err(err).Msgf("giving up retrieving chart values") + return + } + } else { + backOff.Reset() + delay = defaultReaderRefreshRate + } + } + } +} + +func (v *RevValues) refresh(ctx context.Context) error { + if !atomic.CompareAndSwapInt32(&v.inUpdate, 0, 1) { + log.Debug().Msgf("Dropping update...") + return nil + } + defer atomic.StoreInt32(&v.inUpdate, 0) + + if err := v.reconcile(ctx); err != nil { + return err + } + + return nil +} + +func (v *RevValues) reconcile(_ context.Context) error { + v.fireResourceChanged(v.lines, v.filter(v.query, v.lines)) + + return nil +} + +// AddListener adds a new model listener. +func (v *RevValues) AddListener(l ResourceViewerListener) { + v.listeners = append(v.listeners, l) +} + +// RemoveListener delete a listener from the list. +func (v *RevValues) RemoveListener(l ResourceViewerListener) { + victim := -1 + for i, lis := range v.listeners { + if lis == l { + victim = i + break + } + } + + if victim >= 0 { + v.listeners = append(v.listeners[:victim], v.listeners[victim+1:]...) + } +} diff --git a/internal/model/stack_test.go b/internal/model/stack_test.go index aaa43fbcc4..5a3eca62f4 100644 --- a/internal/model/stack_test.go +++ b/internal/model/stack_test.go @@ -301,11 +301,13 @@ func (c c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return func (c 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) 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) {} diff --git a/internal/model/table.go b/internal/model/table.go index 8924ef6c11..c6e84c9fcf 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -17,7 +17,6 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/runtime" ) @@ -57,8 +56,17 @@ func NewTable(gvr client.GVR) *Table { // SetLabelFilter sets the labels filter. func (t *Table) SetLabelFilter(f string) { t.mx.Lock() + defer t.mx.Unlock() + t.labelFilter = f - t.mx.Unlock() +} + +// GetLabelFilter sets the labels filter. +func (t *Table) GetLabelFilter() string { + t.mx.Lock() + defer t.mx.Unlock() + + return t.labelFilter } // SetInstance sets a single entry table. @@ -220,8 +228,9 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err ns := client.CleanseNamespace(t.namespace) if client.IsClusterScoped(t.namespace) { - ns = client.AllNamespaces + ns = client.BlankNamespace } + ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) return a.List(ctx, ns) } @@ -230,9 +239,7 @@ func (t *Table) reconcile(ctx context.Context) error { t.mx.Lock() defer t.mx.Unlock() meta := resourceMeta(t.gvr) - if t.labelFilter != "" { - ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) - } + ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) var ( oo []runtime.Object err error @@ -250,7 +257,7 @@ func (t *Table) reconcile(ctx context.Context) error { var rows render.Rows if len(oo) > 0 { if meta.Renderer.IsGeneric() { - table, ok := oo[0].(*metav1beta1.Table) + table, ok := oo[0].(*metav1.Table) if !ok { return fmt.Errorf("expecting a meta table but got %T", oo[0]) } @@ -312,7 +319,7 @@ func hydrate(ns string, oo []runtime.Object, rr render.Rows, re Renderer) error // Generic represents a generic resource. type Generic interface { // SetTable sets up the resource tabular definition. - SetTable(ns string, table *metav1beta1.Table) + SetTable(ns string, table *metav1.Table) // Header returns a resource header. Header(ns string) render.Header @@ -321,7 +328,7 @@ type Generic interface { Render(o interface{}, ns string, row *render.Row) error } -func genericHydrate(ns string, table *metav1beta1.Table, rr render.Rows, re Renderer) error { +func genericHydrate(ns string, table *metav1.Table, rr render.Rows, re Renderer) error { gr, ok := re.(Generic) if !ok { return fmt.Errorf("expecting generic renderer but got %T", re) diff --git a/internal/model/table_int_test.go b/internal/model/table_int_test.go index abe9a606d2..522cd18c4c 100644 --- a/internal/model/table_int_test.go +++ b/internal/model/table_int_test.go @@ -35,7 +35,7 @@ func TestTableReconcile(t *testing.T) { err := ta.reconcile(ctx) assert.Nil(t, err) data := ta.Peek() - assert.Equal(t, 22, len(data.Header)) + assert.Equal(t, 23, len(data.Header)) assert.Equal(t, 1, len(data.RowEvents)) assert.Equal(t, client.NamespaceAll, data.Namespace) } @@ -108,7 +108,7 @@ func TestTableHydrate(t *testing.T) { assert.Nil(t, hydrate("blee", oo, rr, render.Pod{})) assert.Equal(t, 1, len(rr)) - assert.Equal(t, 22, len(rr[0].Fields)) + assert.Equal(t, 23, len(rr[0].Fields)) } func TestTableGenericHydrate(t *testing.T) { diff --git a/internal/model/table_test.go b/internal/model/table_test.go index b944cc6d5e..ec636b9ed9 100644 --- a/internal/model/table_test.go +++ b/internal/model/table_test.go @@ -36,7 +36,7 @@ func TestTableRefresh(t *testing.T) { ctx = context.WithValue(ctx, internal.KeyWithMetrics, false) assert.NoError(t, ta.Refresh(ctx)) data := ta.Peek() - assert.Equal(t, 22, len(data.Header)) + assert.Equal(t, 23, len(data.Header)) assert.Equal(t, 1, len(data.RowEvents)) assert.Equal(t, client.NamespaceAll, data.Namespace) assert.Equal(t, 1, l.count) diff --git a/internal/model/tree.go b/internal/model/tree.go index f1c60f686c..c013238e85 100644 --- a/internal/model/tree.go +++ b/internal/model/tree.go @@ -17,7 +17,7 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/xray" "github.com/rs/zerolog/log" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -213,7 +213,7 @@ func (t *Tree) reconcile(ctx context.Context) error { root := xray.NewTreeNode(res, res) ctx = context.WithValue(ctx, xray.KeyParent, root) if _, ok := meta.TreeRenderer.(*xray.Generic); ok { - table, ok := oo[0].(*metav1beta1.Table) + table, ok := oo[0].(*metav1.Table) if !ok { return fmt.Errorf("expecting a Table but got %T", oo[0]) } @@ -302,7 +302,7 @@ func treeHydrate(ctx context.Context, ns string, oo []runtime.Object, re TreeRen return nil } -func genericTreeHydrate(ctx context.Context, ns string, table *metav1beta1.Table, re TreeRenderer) error { +func genericTreeHydrate(ctx context.Context, ns string, table *metav1.Table, re TreeRenderer) error { tre, ok := re.(*xray.Generic) if !ok { return fmt.Errorf("expecting xray.Generic renderer but got %T", re) diff --git a/internal/model/types.go b/internal/model/types.go index 00ab43d7d8..0484def5df 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -84,6 +84,12 @@ type Component interface { Igniter Hinter Commander + Filterer +} + +type Filterer interface { + SetFilter(string) + SetLabelFilter(map[string]string) } // Renderer represents a resource renderer. diff --git a/internal/perf/benchmark.go b/internal/perf/benchmark.go index c13f3a0a52..5440e53a90 100644 --- a/internal/perf/benchmark.go +++ b/internal/perf/benchmark.go @@ -11,10 +11,11 @@ import ( "net/http" "os" "path/filepath" + "strings" "sync" "time" - "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" @@ -29,11 +30,6 @@ const ( k9sUA = "k9s/" ) -var ( - // K9sBenchDir directory to store K9s Benchmark files. - K9sBenchDir = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-bench-%s", config.MustK9sUser())) -) - // Benchmark puts a workload under load. type Benchmark struct { canceled bool @@ -111,39 +107,43 @@ func (b *Benchmark) Canceled() bool { } // Run starts a benchmark. -func (b *Benchmark) Run(cluster string, done func()) { - log.Debug().Msgf("Running benchmark on cluster %s", cluster) +func (b *Benchmark) Run(cluster, context string, done func()) { + log.Debug().Msgf("Running benchmark on context %s", cluster) buff := new(bytes.Buffer) b.worker.Writer = buff // this call will block until the benchmark is complete or times out. b.worker.Run() b.worker.Stop() if buff.Len() > 0 { - if err := b.save(cluster, buff); err != nil { + if err := b.save(cluster, context, buff); err != nil { log.Error().Err(err).Msg("Saving Benchmark") } } done() } -func (b *Benchmark) save(cluster string, r io.Reader) error { - dir := filepath.Join(K9sBenchDir, cluster) - if err := os.MkdirAll(dir, 0744); err != nil { +func (b *Benchmark) save(cluster, context string, r io.Reader) error { + ns, n := client.Namespaced(b.config.Name) + n = strings.Replace(n, "|", "_", -1) + n = strings.Replace(n, ":", "_", -1) + dir, err := config.EnsureBenchmarksDir(cluster, context) + if err != nil { + 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 } - ns, n := client.Namespaced(b.config.Name) - file := filepath.Join(dir, fmt.Sprintf(benchFmat, ns, dao.BenchRx.ReplaceAllString(n, "_"), time.Now().UnixNano())) - f, err := os.Create(file) + f, err := os.Create(bf) if err != nil { return err } defer func() { if e := f.Close(); e != nil { - log.Fatal().Err(e).Msg("Bench save") + log.Error().Err(e).Msgf("Benchmark file close failed: %q", bf) } }() - if _, err = io.Copy(f, r); err != nil { return err } diff --git a/internal/port/pf.go b/internal/port/pf.go index 26d4ffbc4c..76f8b6419a 100644 --- a/internal/port/pf.go +++ b/internal/port/pf.go @@ -13,10 +13,10 @@ import ( ) const ( - // K9sAutoPortForwardKey represents an auto portforwards annotation. + // K9sAutoPortForwardsKey represents an auto portforwards annotation. K9sAutoPortForwardsKey = "k9scli.io/auto-port-forwards" - // K9sPortForwardKey represents a portforwards annotation. + // K9sPortForwardsKey represents a portforwards annotation. K9sPortForwardsKey = "k9scli.io/port-forwards" ) diff --git a/internal/render/alias.go b/internal/render/alias.go index 78855128c5..ce8f386d05 100644 --- a/internal/render/alias.go +++ b/internal/render/alias.go @@ -22,7 +22,7 @@ func (Alias) Header(ns string) Header { return Header{ HeaderColumn{Name: "RESOURCE"}, HeaderColumn{Name: "COMMAND"}, - HeaderColumn{Name: "APIGROUP"}, + HeaderColumn{Name: "API-GROUP"}, } } diff --git a/internal/render/alias_test.go b/internal/render/alias_test.go index a9c87a842e..85e61f86ef 100644 --- a/internal/render/alias_test.go +++ b/internal/render/alias_test.go @@ -26,17 +26,17 @@ func TestAliasColorer(t *testing.T) { e tcell.Color }{ "addAll": { - ns: client.AllNamespaces, + ns: client.NamespaceAll, re: render.RowEvent{Kind: render.EventAdd, Row: r}, e: tcell.ColorBlue, }, "deleteAll": { - ns: client.AllNamespaces, + ns: client.NamespaceAll, re: render.RowEvent{Kind: render.EventDelete, Row: r}, e: tcell.ColorGray, }, "updateAll": { - ns: client.AllNamespaces, + ns: client.NamespaceAll, re: render.RowEvent{Kind: render.EventUpdate, Row: r}, e: tcell.ColorDefault, }, @@ -54,12 +54,12 @@ func TestAliasHeader(t *testing.T) { h := render.Header{ render.HeaderColumn{Name: "RESOURCE"}, render.HeaderColumn{Name: "COMMAND"}, - render.HeaderColumn{Name: "APIGROUP"}, + render.HeaderColumn{Name: "API-GROUP"}, } var a render.Alias assert.Equal(t, h, a.Header("fred")) - assert.Equal(t, h, a.Header(client.AllNamespaces)) + assert.Equal(t, h, a.Header(client.NamespaceAll)) } func TestAliasRender(t *testing.T) { diff --git a/internal/render/benchmark.go b/internal/render/benchmark.go index 0f60d4decf..d8a3c75490 100644 --- a/internal/render/benchmark.go +++ b/internal/render/benchmark.go @@ -14,6 +14,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/tcell/v2" "github.com/derailed/tview" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -116,7 +117,7 @@ func (b Benchmark) initRow(row Fields, f os.FileInfo) error { row[0] = tokens[0] row[1] = tokens[1] row[7] = f.Name() - row[9] = timeToAge(f.ModTime()) + row[9] = ToAge(metav1.Time{Time: f.ModTime()}) return nil } diff --git a/internal/render/container.go b/internal/render/container.go index 662441c0e6..b9bd968e1b 100644 --- a/internal/render/container.go +++ b/internal/render/container.go @@ -149,17 +149,30 @@ func (Container) diagnose(state, ready string) error { // ---------------------------------------------------------------------------- // Helpers... +func containerRequests(co *v1.Container) v1.ResourceList { + req := co.Resources.Requests + if len(req) != 0 { + return req + } + lim := co.Resources.Limits + if len(lim) != 0 { + return lim + } + + return nil +} + func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, r metric) { rList, lList := containerRequests(co), co.Resources.Limits if rList.Cpu() != nil { r.cpu = rList.Cpu().MilliValue() } - if lList.Cpu() != nil { - r.lcpu = lList.Cpu().MilliValue() - } if rList.Memory() != nil { r.mem = rList.Memory().Value() } + if lList.Cpu() != nil { + r.lcpu = lList.Cpu().MilliValue() + } if lList.Memory() != nil { r.lmem = lList.Memory().Value() } diff --git a/internal/render/cronjob.go b/internal/render/cronjob.go index aaf2e8fa4d..e75d74f8c8 100644 --- a/internal/render/cronjob.go +++ b/internal/render/cronjob.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/vul" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -26,7 +25,7 @@ func (CronJob) Header(ns string) Header { h := Header{ HeaderColumn{Name: "NAMESPACE"}, HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS"}, + HeaderColumn{Name: "VS", VS: true}, HeaderColumn{Name: "SCHEDULE"}, HeaderColumn{Name: "SUSPEND"}, HeaderColumn{Name: "ACTIVE"}, @@ -38,9 +37,6 @@ func (CronJob) Header(ns string) Header { HeaderColumn{Name: "VALID", Wide: true}, HeaderColumn{Name: "AGE", Time: true}, } - if vul.ImgScanner == nil { - h = append(h[:vulIdx], h[vulIdx+1:]...) - } return h @@ -67,7 +63,7 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error { r.Fields = Fields{ cj.Namespace, cj.Name, - computeVulScore(&cj.Spec.JobTemplate.Spec.Template.Spec), + computeVulScore(cj.ObjectMeta, &cj.Spec.JobTemplate.Spec.Template.Spec), cj.Spec.Schedule, boolPtrToStr(cj.Spec.Suspend), strconv.Itoa(len(cj.Status.Active)), @@ -79,9 +75,6 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error { "", ToAge(cj.GetCreationTimestamp()), } - if vul.ImgScanner == nil { - r.Fields = append(r.Fields[:vulIdx], r.Fields[vulIdx+1:]...) - } return nil } diff --git a/internal/render/cronjob_test.go b/internal/render/cronjob_test.go index e24ac7852a..ff11bd9bf8 100644 --- a/internal/render/cronjob_test.go +++ b/internal/render/cronjob_test.go @@ -16,5 +16,5 @@ func TestCronJobRender(t *testing.T) { assert.NoError(t, c.Render(load(t, "cj"), "", &r)) assert.Equal(t, "default/hello", r.ID) - assert.Equal(t, render.Fields{"default", "hello", "*/1 * * * *", "false", "0"}, r.Fields[:5]) + assert.Equal(t, render.Fields{"default", "hello", "0", "*/1 * * * *", "false", "0"}, r.Fields[:6]) } diff --git a/internal/render/dp.go b/internal/render/dp.go index 3d1b2b5534..01eb96e69f 100644 --- a/internal/render/dp.go +++ b/internal/render/dp.go @@ -6,9 +6,10 @@ package render import ( "fmt" "strconv" + "strings" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/vul" + "github.com/derailed/tcell/v2" "github.com/derailed/tview" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -22,7 +23,23 @@ type Deployment struct { // ColorerFunc colors a resource row. func (d Deployment) ColorerFunc() ColorerFunc { - return DefaultColorer + return func(ns string, h Header, re RowEvent) tcell.Color { + c := DefaultColorer(ns, h, re) + if !Happy(ns, h, re.Row) { + return ErrColor + } + rdCol := h.IndexOf("READY", true) + if rdCol == -1 { + return c + } + ready := strings.TrimSpace(re.Row.Fields[rdCol]) + tt := strings.Split(ready, "/") + if len(tt) == 2 && tt[1] == "0" { + return PendingColor + } + + return c + } } // Header returns a header row. @@ -30,7 +47,7 @@ func (Deployment) Header(ns string) Header { h := Header{ HeaderColumn{Name: "NAMESPACE"}, HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS"}, + HeaderColumn{Name: "VS", VS: true}, HeaderColumn{Name: "READY", Align: tview.AlignRight}, HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight}, HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight}, @@ -38,9 +55,6 @@ func (Deployment) Header(ns string) Header { HeaderColumn{Name: "VALID", Wide: true}, HeaderColumn{Name: "AGE", Time: true}, } - if vul.ImgScanner == nil { - h = append(h[:vulIdx], h[vulIdx+1:]...) - } return h } @@ -62,7 +76,7 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error { r.Fields = Fields{ dp.Namespace, dp.Name, - computeVulScore(&dp.Spec.Template.Spec), + computeVulScore(dp.ObjectMeta, &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)), @@ -70,9 +84,6 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error { AsStatus(d.diagnose(dp.Status.Replicas, dp.Status.AvailableReplicas)), ToAge(dp.GetCreationTimestamp()), } - if vul.ImgScanner == nil { - r.Fields = append(r.Fields[:vulIdx], r.Fields[vulIdx+1:]...) - } return nil } @@ -81,5 +92,6 @@ func (Deployment) diagnose(desired, avail int32) error { if desired != avail { return fmt.Errorf("desiring %d replicas got %d available", desired, avail) } + return nil } diff --git a/internal/render/dp_test.go b/internal/render/dp_test.go index c82a1defa4..92653b1864 100644 --- a/internal/render/dp_test.go +++ b/internal/render/dp_test.go @@ -16,7 +16,7 @@ func TestDpRender(t *testing.T) { assert.Nil(t, c.Render(load(t, "dp"), "", &r)) assert.Equal(t, "icx/icx-db", r.ID) - assert.Equal(t, render.Fields{"icx", "icx-db", "1/1", "1", "1"}, r.Fields[:5]) + assert.Equal(t, render.Fields{"icx", "icx-db", "0", "1/1", "1", "1"}, r.Fields[:6]) } func BenchmarkDpRender(b *testing.B) { diff --git a/internal/render/ds.go b/internal/render/ds.go index 87f92494ae..d14a1569b5 100644 --- a/internal/render/ds.go +++ b/internal/render/ds.go @@ -8,7 +8,6 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/vul" "github.com/derailed/tview" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -25,7 +24,7 @@ func (DaemonSet) Header(ns string) Header { h := Header{ HeaderColumn{Name: "NAMESPACE"}, HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS"}, + HeaderColumn{Name: "VS", VS: true}, HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, HeaderColumn{Name: "READY", Align: tview.AlignRight}, @@ -35,9 +34,6 @@ func (DaemonSet) Header(ns string) Header { HeaderColumn{Name: "VALID", Wide: true}, HeaderColumn{Name: "AGE", Time: true}, } - if vul.ImgScanner == nil { - h = append(h[:vulIdx], h[vulIdx+1:]...) - } return h } @@ -58,7 +54,7 @@ func (d DaemonSet) Render(o interface{}, ns string, r *Row) error { r.Fields = Fields{ ds.Namespace, ds.Name, - computeVulScore(&ds.Spec.Template.Spec), + computeVulScore(ds.ObjectMeta, &ds.Spec.Template.Spec), strconv.Itoa(int(ds.Status.DesiredNumberScheduled)), strconv.Itoa(int(ds.Status.CurrentNumberScheduled)), strconv.Itoa(int(ds.Status.NumberReady)), @@ -68,9 +64,6 @@ func (d DaemonSet) Render(o interface{}, ns string, r *Row) error { AsStatus(d.diagnose(ds.Status.DesiredNumberScheduled, ds.Status.NumberReady)), ToAge(ds.GetCreationTimestamp()), } - if vul.ImgScanner == nil { - r.Fields = append(r.Fields[:vulIdx], r.Fields[vulIdx+1:]...) - } return nil } diff --git a/internal/render/ds_test.go b/internal/render/ds_test.go index a493544545..5753bcb6a4 100644 --- a/internal/render/ds_test.go +++ b/internal/render/ds_test.go @@ -16,5 +16,5 @@ func TestDaemonSetRender(t *testing.T) { assert.NoError(t, c.Render(load(t, "ds"), "", &r)) assert.Equal(t, "kube-system/fluentd-gcp-v3.2.0", r.ID) - assert.Equal(t, render.Fields{"kube-system", "fluentd-gcp-v3.2.0", "2", "2", "2", "2", "2"}, r.Fields[:7]) + assert.Equal(t, render.Fields{"kube-system", "fluentd-gcp-v3.2.0", "0", "2", "2", "2", "2", "2"}, r.Fields[:8]) } diff --git a/internal/render/ev.go b/internal/render/ev.go index f028c23844..f2e8b9b1f1 100644 --- a/internal/render/ev.go +++ b/internal/render/ev.go @@ -9,7 +9,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/tcell/v2" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Event renders a K8s Event to screen. @@ -68,7 +68,7 @@ func (e *Event) Header(ns string) Header { // Render renders a K8s resource to screen. func (e *Event) Render(o interface{}, ns string, r *Row) error { - row, ok := o.(metav1beta1.TableRow) + row, ok := o.(metav1.TableRow) if !ok { return fmt.Errorf("expecting a TableRow but got %T", o) } diff --git a/internal/render/generic.go b/internal/render/generic.go index 0297f940f4..fc89fdbefa 100644 --- a/internal/render/generic.go +++ b/internal/render/generic.go @@ -11,8 +11,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" - - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ageTableCol = "Age" @@ -20,7 +19,7 @@ const ageTableCol = "Age" // Generic renders a generic resource to screen. type Generic struct { Base - table *metav1beta1.Table + table *metav1.Table header Header ageIndex int } @@ -30,7 +29,7 @@ func (*Generic) IsGeneric() bool { } // SetTable sets the tabular resource. -func (g *Generic) SetTable(ns string, t *metav1beta1.Table) { +func (g *Generic) SetTable(ns string, t *metav1.Table) { g.table = t g.header = g.Header(ns) } @@ -68,7 +67,7 @@ func (g *Generic) Header(ns string) Header { // Render renders a K8s resource to screen. func (g *Generic) Render(o interface{}, ns string, r *Row) error { - row, ok := o.(metav1beta1.TableRow) + row, ok := o.(metav1.TableRow) if !ok { return fmt.Errorf("expecting a TableRow but got %T", o) } diff --git a/internal/render/generic_test.go b/internal/render/generic_test.go index 07172d2965..851aaa7290 100644 --- a/internal/render/generic_test.go +++ b/internal/render/generic_test.go @@ -46,7 +46,7 @@ func TestGenericRender(t *testing.T) { }, }, "allNS": { - ns: client.AllNamespaces, + ns: client.NamespaceAll, table: makeNSGeneric(), eID: "ns1/fred", eFields: render.Fields{"ns1", "c1", "c2", "c3"}, diff --git a/internal/render/header.go b/internal/render/header.go index 780cc3d073..5ac9bcd2fb 100644 --- a/internal/render/header.go +++ b/internal/render/header.go @@ -21,6 +21,7 @@ type HeaderColumn struct { MX bool Time bool Capacity bool + VS bool } // Clone copies a header. diff --git a/internal/render/helpers.go b/internal/render/helpers.go index e6945c33a0..548912f8e0 100644 --- a/internal/render/helpers.go +++ b/internal/render/helpers.go @@ -23,8 +23,8 @@ import ( "k8s.io/apimachinery/pkg/util/duration" ) -func computeVulScore(spec *v1.PodSpec) string { - if vul.ImgScanner == nil { +func computeVulScore(m metav1.ObjectMeta, spec *v1.PodSpec) string { + if vul.ImgScanner == nil || vul.ImgScanner.ShouldExcludes(m) { return "0" } ii := ExtractImages(spec) diff --git a/internal/render/job.go b/internal/render/job.go index b6325d8cf9..8fda057b98 100644 --- a/internal/render/job.go +++ b/internal/render/job.go @@ -10,7 +10,6 @@ import ( "time" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/vul" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -29,7 +28,7 @@ func (Job) Header(ns string) Header { h := Header{ HeaderColumn{Name: "NAMESPACE"}, HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS"}, + HeaderColumn{Name: "VS", VS: true}, HeaderColumn{Name: "COMPLETIONS"}, HeaderColumn{Name: "DURATION"}, HeaderColumn{Name: "SELECTOR", Wide: true}, @@ -38,9 +37,6 @@ func (Job) Header(ns string) Header { HeaderColumn{Name: "VALID", Wide: true}, HeaderColumn{Name: "AGE", Time: true}, } - if vul.ImgScanner == nil { - h = append(h[:vulIdx], h[vulIdx+1:]...) - } return h } @@ -64,7 +60,7 @@ func (j Job) Render(o interface{}, ns string, r *Row) error { r.Fields = Fields{ job.Namespace, job.Name, - computeVulScore(&job.Spec.Template.Spec), + computeVulScore(job.ObjectMeta, &job.Spec.Template.Spec), ready, toDuration(job.Status), jobSelector(job.Spec), @@ -73,9 +69,6 @@ func (j Job) Render(o interface{}, ns string, r *Row) error { AsStatus(j.diagnose(ready, job.Status.CompletionTime)), ToAge(job.GetCreationTimestamp()), } - if vul.ImgScanner == nil { - r.Fields = append(r.Fields[:vulIdx], r.Fields[vulIdx+1:]...) - } return nil } diff --git a/internal/render/job_test.go b/internal/render/job_test.go index 5479291270..b26179966a 100644 --- a/internal/render/job_test.go +++ b/internal/render/job_test.go @@ -16,5 +16,5 @@ func TestJobRender(t *testing.T) { assert.NoError(t, c.Render(load(t, "job"), "", &r)) assert.Equal(t, "default/hello-1567179180", r.ID) - assert.Equal(t, render.Fields{"default", "hello-1567179180", "1/1", "8s", "controller-uid=7473e6d0-cb3b-11e9-990f-42010a800218", "c1", "blang/busybox-bash"}, r.Fields[:7]) + assert.Equal(t, render.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/ns.go b/internal/render/ns.go index ddf34cf6e4..77954a5eda 100644 --- a/internal/render/ns.go +++ b/internal/render/ns.go @@ -79,5 +79,6 @@ func (Namespace) diagnose(phase v1.NamespacePhase) error { if phase != v1.NamespaceActive && phase != v1.NamespaceTerminating { return errors.New("namespace not ready") } + return nil } diff --git a/internal/render/pod.go b/internal/render/pod.go index a79cafe252..fb7ab88d57 100644 --- a/internal/render/pod.go +++ b/internal/render/pod.go @@ -18,7 +18,6 @@ import ( mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/vul" ) const ( @@ -41,6 +40,7 @@ const ( PhaseError = "Error" PhaseImagePullBackOff = "ImagePullBackOff" PhaseOOMKilled = "OOMKilled" + PhasePending = "Pending" ) // Pod renders a K8s Pod to screen. @@ -59,9 +59,9 @@ func (p Pod) ColorerFunc() ColorerFunc { } status := strings.TrimSpace(re.Row.Fields[statusCol]) switch status { - case Pending: + case Pending, ContainerCreating: c = PendingColor - case ContainerCreating, PodInitializing: + case PodInitializing: c = AddColor case Initialized: c = HighlightColor @@ -88,15 +88,11 @@ func (Pod) Header(ns string) Header { h := Header{ HeaderColumn{Name: "NAMESPACE"}, HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS"}, + HeaderColumn{Name: "VS", VS: true}, HeaderColumn{Name: "PF"}, HeaderColumn{Name: "READY"}, HeaderColumn{Name: "STATUS"}, HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, - HeaderColumn{Name: "IP"}, - HeaderColumn{Name: "NODE"}, - HeaderColumn{Name: "NOMINATED NODE", Wide: true}, - HeaderColumn{Name: "READINESS GATES", Wide: true}, HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight, Wide: true}, @@ -105,14 +101,15 @@ func (Pod) Header(ns string) Header { HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true}, + HeaderColumn{Name: "IP", Wide: true}, + HeaderColumn{Name: "NODE", Wide: true}, + HeaderColumn{Name: "NOMINATED NODE", Wide: true}, + HeaderColumn{Name: "READINESS GATES", Wide: true}, HeaderColumn{Name: "QOS", Wide: true}, HeaderColumn{Name: "LABELS", Wide: true}, HeaderColumn{Name: "VALID", Wide: true}, HeaderColumn{Name: "AGE", Time: true}, } - if vul.ImgScanner == nil { - h = append(h[:vulIdx], h[vulIdx+1:]...) - } return h } @@ -151,15 +148,11 @@ func (p Pod) Render(o interface{}, ns string, row *Row) error { row.Fields = Fields{ po.Namespace, po.ObjectMeta.Name, - computeVulScore(&po.Spec), + computeVulScore(po.ObjectMeta, &po.Spec), "●", strconv.Itoa(cr) + "/" + strconv.Itoa(len(po.Spec.Containers)), phase, strconv.Itoa(rc + irc), - na(po.Status.PodIP), - na(po.Spec.NodeName), - asNominated(po.Status.NominatedNodeName), - asReadinessGate(po), toMc(c.cpu), toMi(c.mem), toMc(r.cpu) + ":" + toMc(r.lcpu), @@ -168,14 +161,15 @@ func (p Pod) Render(o interface{}, ns string, row *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), + asNominated(po.Status.NominatedNodeName), + asReadinessGate(po), p.mapQOS(po.Status.QOSClass), mapToStr(po.Labels), AsStatus(p.diagnose(phase, cr, len(cs))), ToAge(po.GetCreationTimestamp()), } - if vul.ImgScanner == nil { - row.Fields = append(row.Fields[:vulIdx], row.Fields[vulIdx+1:]...) - } return nil } @@ -239,9 +233,12 @@ func (p *PodWithMetrics) DeepCopyObject() runtime.Object { } func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (c, r metric) { - rcpu, rmem := podRequests(pod.Spec) - lcpu, lmem := podLimits(pod.Spec) - r.cpu, r.lcpu, r.mem, r.lmem = rcpu.MilliValue(), lcpu.MilliValue(), rmem.Value(), lmem.Value() + rcpu, rmem := podRequests(pod.Spec.Containers) + r.cpu, r.mem = rcpu.MilliValue(), rmem.Value() + + lcpu, lmem := podLimits(pod.Spec.Containers) + r.lcpu, r.lmem = lcpu.MilliValue(), lmem.Value() + if mx != nil { ccpu, cmem := currentRes(mx) c.cpu, c.mem = ccpu.MilliValue(), cmem.Value() @@ -250,23 +247,10 @@ func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (c, r metric) { return } -func containerRequests(co *v1.Container) v1.ResourceList { - req := co.Resources.Requests - if len(req) != 0 { - return req - } - lim := co.Resources.Limits - if len(lim) != 0 { - return lim - } - - return nil -} - -func podLimits(spec v1.PodSpec) (resource.Quantity, resource.Quantity) { +func podLimits(cc []v1.Container) (resource.Quantity, resource.Quantity) { cpu, mem := new(resource.Quantity), new(resource.Quantity) - for _, co := range spec.Containers { - limits := co.Resources.Limits + for _, c := range cc { + limits := c.Resources.Limits if len(limits) == 0 { return resource.Quantity{}, resource.Quantity{} } @@ -280,10 +264,11 @@ func podLimits(spec v1.PodSpec) (resource.Quantity, resource.Quantity) { return *cpu, *mem } -func podRequests(spec v1.PodSpec) (resource.Quantity, resource.Quantity) { +func podRequests(cc []v1.Container) (resource.Quantity, resource.Quantity) { cpu, mem := new(resource.Quantity), new(resource.Quantity) - for i := range spec.Containers { - rl := containerRequests(&spec.Containers[i]) + for _, c := range cc { + co := c + rl := containerRequests(&co) if rl.Cpu() != nil { cpu.Add(*rl.Cpu()) } @@ -291,6 +276,7 @@ func podRequests(spec v1.PodSpec) (resource.Quantity, resource.Quantity) { mem.Add(*rl.Memory()) } } + return *cpu, *mem } diff --git a/internal/render/pod_test.go b/internal/render/pod_test.go index c442c2e50f..32a89b1b8a 100644 --- a/internal/render/pod_test.go +++ b/internal/render/pod_test.go @@ -162,8 +162,8 @@ func TestPodRender(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "default/nginx", r.ID) - e := render.Fields{"default", "nginx", "●", "1/1", "Running", "0", "172.17.0.6", "minikube", "", "", "100", "50", "100:0", "70:170", "100", "n/a", "71"} - assert.Equal(t, e, r.Fields[:17]) + e := render.Fields{"default", "nginx", "0", "●", "1/1", "Running", "0", "100", "50", "100:0", "70:170", "100", "n/a", "71", "29", "172.17.0.6", "minikube", "", ""} + assert.Equal(t, e, r.Fields[:19]) } func BenchmarkPodRender(b *testing.B) { @@ -193,8 +193,8 @@ func TestPodInitRender(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "default/nginx", r.ID) - e := render.Fields{"default", "nginx", "●", "1/1", "Init:0/1", "0", "172.17.0.6", "minikube", "", "", "10", "10", "100:0", "70:170", "10", "n/a", "14"} - assert.Equal(t, e, r.Fields[:17]) + e := render.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", "", ""} + assert.Equal(t, e, r.Fields[:19]) } func TestCheckPodStatus(t *testing.T) { diff --git a/internal/render/policy.go b/internal/render/policy.go index 568ac540c8..e750bcb0fd 100644 --- a/internal/render/policy.go +++ b/internal/render/policy.go @@ -44,7 +44,7 @@ func (Policy) Header(ns string) Header { h := Header{ HeaderColumn{Name: "NAMESPACE"}, HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "API GROUP"}, + HeaderColumn{Name: "API-GROUP"}, HeaderColumn{Name: "BINDING"}, } h = append(h, rbacVerbHeader()...) diff --git a/internal/render/port_forward_test.go b/internal/render/port_forward_test.go index 4084cb40c5..c3d0d9c226 100644 --- a/internal/render/port_forward_test.go +++ b/internal/render/port_forward_test.go @@ -5,14 +5,13 @@ package render_test import ( "testing" + "time" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestPortForwardRender(t *testing.T) { - var p render.PortForward - var r render.Row o := render.ForwardRes{ Forwarder: fwd{}, Config: render.BenchCfg{ @@ -23,6 +22,8 @@ func TestPortForwardRender(t *testing.T) { }, } + var p render.PortForward + var r render.Row assert.Nil(t, p.Render(o, "fred", &r)) assert.Equal(t, "blee/fred", r.ID) assert.Equal(t, render.Fields{ @@ -34,8 +35,7 @@ func TestPortForwardRender(t *testing.T) { "1", "1", "", - "2m", - }, r.Fields) + }, r.Fields[:8]) } // Helpers... @@ -62,6 +62,6 @@ func (f fwd) Active() bool { return true } -func (f fwd) Age() string { - return "2m" +func (f fwd) Age() time.Time { + return testTime() } diff --git a/internal/render/portforward.go b/internal/render/portforward.go index d3908bb9d9..5ae2f71eee 100644 --- a/internal/render/portforward.go +++ b/internal/render/portforward.go @@ -6,9 +6,11 @@ package render import ( "fmt" "strings" + "time" "github.com/derailed/k9s/internal/client" "github.com/derailed/tcell/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -28,7 +30,7 @@ type Forwarder interface { Active() bool // Age returns forwarder age. - Age() string + Age() time.Time } // PortForward renders a portforwards to screen. @@ -78,7 +80,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error { AsThousands(int64(pf.Config.C)), AsThousands(int64(pf.Config.N)), "", - pf.Age(), + ToAge(metav1.Time{Time: pf.Age()}), } return nil diff --git a/internal/render/rbac.go b/internal/render/rbac.go index 5af09c87e7..ec23c8ddb2 100644 --- a/internal/render/rbac.go +++ b/internal/render/rbac.go @@ -46,7 +46,7 @@ func (Rbac) Header(ns string) Header { h := make(Header, 0, 10) h = append(h, HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "APIGROUP"}, + HeaderColumn{Name: "API-GROUP"}, ) h = append(h, rbacVerbHeader()...) diff --git a/internal/render/rs.go b/internal/render/rs.go index a8287e4566..6dd7f38f1e 100644 --- a/internal/render/rs.go +++ b/internal/render/rs.go @@ -8,7 +8,6 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/vul" "github.com/derailed/tview" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -30,7 +29,7 @@ func (ReplicaSet) Header(ns string) Header { h := Header{ HeaderColumn{Name: "NAMESPACE"}, HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS"}, + HeaderColumn{Name: "VS", VS: true}, HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, HeaderColumn{Name: "READY", Align: tview.AlignRight}, @@ -38,9 +37,6 @@ func (ReplicaSet) Header(ns string) Header { HeaderColumn{Name: "VALID", Wide: true}, HeaderColumn{Name: "AGE", Time: true}, } - if vul.ImgScanner == nil { - h = append(h[:vulIdx], h[vulIdx+1:]...) - } return h } @@ -61,7 +57,7 @@ func (r ReplicaSet) Render(o interface{}, ns string, row *Row) error { row.Fields = Fields{ rs.Namespace, rs.Name, - computeVulScore(&rs.Spec.Template.Spec), + computeVulScore(rs.ObjectMeta, &rs.Spec.Template.Spec), strconv.Itoa(int(*rs.Spec.Replicas)), strconv.Itoa(int(rs.Status.Replicas)), strconv.Itoa(int(rs.Status.ReadyReplicas)), @@ -69,9 +65,6 @@ func (r ReplicaSet) Render(o interface{}, ns string, row *Row) error { AsStatus(r.diagnose(rs)), ToAge(rs.GetCreationTimestamp()), } - if vul.ImgScanner == nil { - row.Fields = append(row.Fields[:vulIdx], row.Fields[vulIdx+1:]...) - } return nil } diff --git a/internal/render/rs_test.go b/internal/render/rs_test.go index 066f5e0878..8a85dc599a 100644 --- a/internal/render/rs_test.go +++ b/internal/render/rs_test.go @@ -16,5 +16,5 @@ func TestReplicaSetRender(t *testing.T) { assert.NoError(t, c.Render(load(t, "rs"), "", &r)) assert.Equal(t, "icx/icx-db-7d4b578979", r.ID) - assert.Equal(t, render.Fields{"icx", "icx-db-7d4b578979", "1", "1", "1"}, r.Fields[:5]) + assert.Equal(t, render.Fields{"icx", "icx-db-7d4b578979", "0", "1", "1", "1"}, r.Fields[:6]) } diff --git a/internal/render/sts.go b/internal/render/sts.go index 08704bfd9c..cc6b05232a 100644 --- a/internal/render/sts.go +++ b/internal/render/sts.go @@ -8,7 +8,6 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/vul" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -24,7 +23,7 @@ func (StatefulSet) Header(ns string) Header { h := Header{ HeaderColumn{Name: "NAMESPACE"}, HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS"}, + HeaderColumn{Name: "VS", VS: true}, HeaderColumn{Name: "READY"}, HeaderColumn{Name: "SELECTOR", Wide: true}, HeaderColumn{Name: "SERVICE"}, @@ -34,9 +33,6 @@ func (StatefulSet) Header(ns string) Header { HeaderColumn{Name: "VALID", Wide: true}, HeaderColumn{Name: "AGE", Time: true}, } - if vul.ImgScanner == nil { - h = append(h[:vulIdx], h[vulIdx+1:]...) - } return h } @@ -57,7 +53,7 @@ func (s StatefulSet) Render(o interface{}, ns string, r *Row) error { r.Fields = Fields{ sts.Namespace, sts.Name, - computeVulScore(&sts.Spec.Template.Spec), + computeVulScore(sts.ObjectMeta, &sts.Spec.Template.Spec), strconv.Itoa(int(sts.Status.ReadyReplicas)) + "/" + strconv.Itoa(int(sts.Status.Replicas)), asSelector(sts.Spec.Selector), na(sts.Spec.ServiceName), @@ -67,9 +63,6 @@ func (s StatefulSet) Render(o interface{}, ns string, r *Row) error { AsStatus(s.diagnose(sts.Status.Replicas, sts.Status.ReadyReplicas)), ToAge(sts.GetCreationTimestamp()), } - if vul.ImgScanner == nil { - r.Fields = append(r.Fields[:vulIdx], r.Fields[vulIdx+1:]...) - } return nil } diff --git a/internal/render/sts_test.go b/internal/render/sts_test.go index f3f9c7bd39..0070d1a818 100644 --- a/internal/render/sts_test.go +++ b/internal/render/sts_test.go @@ -16,5 +16,5 @@ func TestStatefulSetRender(t *testing.T) { assert.Nil(t, c.Render(load(t, "sts"), "", &r)) assert.Equal(t, "default/nginx-sts", r.ID) - assert.Equal(t, render.Fields{"default", "nginx-sts", "4/4", "app=nginx-sts", "nginx-sts", "nginx", "k8s.gcr.io/nginx-slim:0.8", "app=nginx-sts", ""}, r.Fields[:len(r.Fields)-1]) + assert.Equal(t, render.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/workload.go b/internal/render/workload.go new file mode 100644 index 0000000000..058cfd4078 --- /dev/null +++ b/internal/render/workload.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package render + +import ( + "fmt" + "strings" + + "github.com/derailed/tcell/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Workload renders a workload to screen. +type Workload struct { + Base +} + +// ColorerFunc colors a resource row. +func (n Workload) ColorerFunc() ColorerFunc { + return func(ns string, h Header, re RowEvent) tcell.Color { + c := DefaultColorer(ns, h, re) + + statusCol := h.IndexOf("STATUS", true) + if statusCol == -1 { + return c + } + status := strings.TrimSpace(re.Row.Fields[statusCol]) + if status == "DEGRADED" { + c = PendingColor + } + + return c + } +} + +// Header returns a header rbw. +func (Workload) Header(string) Header { + return Header{ + HeaderColumn{Name: "KIND"}, + HeaderColumn{Name: "NAMESPACE"}, + HeaderColumn{Name: "NAME"}, + HeaderColumn{Name: "STATUS"}, + HeaderColumn{Name: "READY"}, + HeaderColumn{Name: "AGE", Time: true}, + } +} + +// Render renders a K8s resource to screen. +func (n Workload) Render(o interface{}, _ string, r *Row) error { + res, ok := o.(*WorkloadRes) + if !ok { + return fmt.Errorf("expected allRes 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)) + r.Fields = Fields{ + res.Row.Cells[0].(string), + res.Row.Cells[1].(string), + res.Row.Cells[2].(string), + res.Row.Cells[3].(string), + res.Row.Cells[4].(string), + ToAge(res.Row.Cells[5].(metav1.Time)), + } + + return nil +} + +type WorkloadRes struct { + Row metav1.TableRow +} + +// GetObjectKind returns a schema object. +func (a *WorkloadRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (a *WorkloadRes) DeepCopyObject() runtime.Object { + return a +} diff --git a/internal/ui/app.go b/internal/ui/app.go index 3b656af2d1..b7b4d22816 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -44,7 +44,7 @@ func NewApp(cfg *config.Config, context string) *App { a.views = map[string]tview.Primitive{ "menu": NewMenu(a.Styles), "logo": NewLogo(a.Styles), - "prompt": NewPrompt(&a, a.Config.K9s.NoIcons, a.Styles), + "prompt": NewPrompt(&a, a.Config.K9s.UI.NoIcons, a.Styles), "crumbs": NewCrumbs(a.Styles), } @@ -58,7 +58,7 @@ func (a *App) Init() { a.cmdBuff.AddListener(a) a.Styles.AddListener(a) - a.SetRoot(a.Main, true).EnableMouse(a.Config.K9s.EnableMouse) + a.SetRoot(a.Main, true).EnableMouse(a.Config.K9s.UI.EnableMouse) } // QueueUpdate queues up a ui action. diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go index a49cb34f58..36d5c6ff02 100644 --- a/internal/ui/app_test.go +++ b/internal/ui/app_test.go @@ -6,13 +6,13 @@ package ui_test import ( "testing" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" ) func TestAppGetCmd(t *testing.T) { - a := ui.NewApp(config.NewConfig(nil), "") + a := ui.NewApp(mock.NewMockConfig(), "") a.Init() a.CmdBuff().SetText("blee", "") @@ -20,7 +20,7 @@ func TestAppGetCmd(t *testing.T) { } func TestAppInCmdMode(t *testing.T) { - a := ui.NewApp(config.NewConfig(nil), "") + a := ui.NewApp(mock.NewMockConfig(), "") a.Init() a.CmdBuff().SetText("blee", "") assert.False(t, a.InCmdMode()) @@ -30,7 +30,7 @@ func TestAppInCmdMode(t *testing.T) { } func TestAppResetCmd(t *testing.T) { - a := ui.NewApp(config.NewConfig(nil), "") + a := ui.NewApp(mock.NewMockConfig(), "") a.Init() a.CmdBuff().SetText("blee", "") @@ -40,7 +40,7 @@ func TestAppResetCmd(t *testing.T) { } func TestAppHasCmd(t *testing.T) { - a := ui.NewApp(config.NewConfig(nil), "") + a := ui.NewApp(mock.NewMockConfig(), "") a.Init() a.ActivateCmd(true) @@ -51,7 +51,7 @@ func TestAppHasCmd(t *testing.T) { } func TestAppGetActions(t *testing.T) { - a := ui.NewApp(config.NewConfig(nil), "") + a := ui.NewApp(mock.NewMockConfig(), "") a.Init() a.AddActions(ui.KeyActions{ui.KeyZ: ui.KeyAction{Description: "zorg"}}) @@ -60,7 +60,7 @@ func TestAppGetActions(t *testing.T) { } func TestAppViews(t *testing.T) { - a := ui.NewApp(config.NewConfig(nil), "") + a := ui.NewApp(mock.NewMockConfig(), "") a.Init() vv := []string{"crumbs", "logo", "prompt", "menu"} diff --git a/internal/ui/config.go b/internal/ui/config.go index b00196fff7..d6f3bc7698 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -6,9 +6,7 @@ package ui import ( "context" "errors" - "fmt" "os" - "path/filepath" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/render" @@ -47,16 +45,18 @@ func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) e for { select { case evt := <-w.Events: - if evt.Name == config.K9sViewConfigFile { + if evt.Name == config.AppViewsFile { s.QueueUpdateDraw(func() { - c.RefreshCustomViews() + if err := c.RefreshCustomViews(); err != nil { + log.Warn().Err(err).Msgf("Custom views refresh failed") + } }) } case err := <-w.Errors: log.Warn().Err(err).Msg("CustomView watcher failed") return case <-ctx.Done(): - log.Debug().Msgf("CustomViewWatcher CANCELED `%s!!", config.K9sViewConfigFile) + log.Debug().Msgf("CustomViewWatcher CANCELED `%s!!", config.AppViewsFile) if err := w.Close(); err != nil { log.Error().Err(err).Msg("Closing CustomView watcher") } @@ -65,23 +65,21 @@ func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) e } }() - log.Debug().Msgf("CustomView watching `%s", config.K9sViewConfigFile) - c.RefreshCustomViews() - return w.Add(config.K9sHome()) + if err := c.RefreshCustomViews(); err != nil { + return err + } + return w.Add(config.AppViewsFile) } // RefreshCustomViews load view configuration changes. -func (c *Configurator) RefreshCustomViews() { +func (c *Configurator) RefreshCustomViews() error { if c.CustomView == nil { c.CustomView = config.NewCustomView() } else { c.CustomView.Reset() } - if err := c.CustomView.Load(config.K9sViewConfigFile); err != nil { - log.Warn().Err(err).Msgf("Custom view load failed %s", config.K9sViewConfigFile) - return - } + return c.CustomView.Load(config.AppViewsFile) } // StylesWatcher watches for skin file changes. @@ -102,7 +100,7 @@ func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error if evt.Name == c.skinFile && evt.Op != fsnotify.Chmod { log.Debug().Msgf("Skin changed: %s", c.skinFile) s.QueueUpdateDraw(func() { - c.RefreshStyles(c.Config.K9s.CurrentCluster) + c.RefreshStyles(c.Config.K9s.ActiveContextName()) }) } case err := <-w.Errors: @@ -122,42 +120,25 @@ func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error if err := w.Add(config.K9sHome()); err != nil { return err } - log.Debug().Msgf("SkinWatcher watching %q", config.K9sSkinDir) - return w.Add(config.K9sSkinDir) + log.Debug().Msgf("SkinWatcher watching %q", config.AppSkinsDir) + return w.Add(config.AppSkinsDir) } -// BenchConfig location of the benchmarks configuration file. -func BenchConfig(context string) string { - return filepath.Join(config.K9sHome(), config.K9sBench+"-"+context+".yml") -} - -func (c *Configurator) clusterFromContext(name string) (*config.Cluster, error) { - if c.Config == nil || c.Config.GetConnection() == nil { - return nil, fmt.Errorf("No config set in configurator") - } - - cc, err := c.Config.GetConnection().Config().Contexts() - if err != nil { - return nil, errors.New("unable to retrieve contexts map") - } - - context, ok := cc[name] - if !ok { - return nil, fmt.Errorf("no context named %s found", name) +// RefreshStyles load for skin configuration changes. +func (c *Configurator) RefreshStyles(context string) { + cluster := "na" + if c.Config != nil { + if ct, err := c.Config.K9s.ActiveContext(); err == nil { + cluster = ct.ClusterName + } } - cl, ok := c.Config.K9s.Clusters[context.Cluster] - if !ok { - return nil, fmt.Errorf("no cluster named %s found", context.Cluster) + if bc, err := config.EnsureBenchmarksCfgFile(cluster, context); err != nil { + log.Warn().Err(err).Msgf("No benchmark config file found for context: %s", context) + } else { + c.BenchFile = bc } - return cl, nil -} - -// RefreshStyles load for skin configuration changes. -func (c *Configurator) RefreshStyles(context string) { - c.BenchFile = BenchConfig(context) - if c.Styles == nil { c.Styles = config.NewStyles() } else { @@ -165,39 +146,29 @@ func (c *Configurator) RefreshStyles(context string) { } var skin string - cl, err := c.clusterFromContext(context) - if err != nil { - log.Warn().Err(err).Msgf("No cluster found. Using default skin") - } else { - skin = cl.Skin - } - - var ( - skinFile = filepath.Join(config.K9sSkinDir, skin+".yml") - ) - if skin != "" { - if err := c.Styles.Load(skinFile); err != nil { - if errors.Is(err, os.ErrNotExist) { - log.Warn().Msgf("Skin file %q not found in skins dir: %s", skinFile, config.K9sSkinDir) - } else { - log.Error().Msgf("Failed to parse skin file -- %s: %s.", skinFile, err) - } - } else { - c.updateStyles(skinFile) - return + if c.Config != nil { + skin = c.Config.K9s.UI.Skin + ct, err := c.Config.K9s.ActiveContext() + if err != nil { + log.Warn().Msgf("No active context found. Using default skin") + } else if ct.Skin != "" { + skin = ct.Skin } } - - if err := c.Styles.Load(config.K9sStylesFile); err != nil { + if skin == "" { + c.updateStyles("") + return + } + var skinFile = config.SkinFileFromName(skin) + if err := c.Styles.Load(skinFile); err != nil { if errors.Is(err, os.ErrNotExist) { - log.Warn().Msgf("No skin file found -- %s. Loading stock skins.", config.K9sStylesFile) + log.Warn().Msgf("Skin file %q not found in skins dir: %s", skinFile, config.AppSkinsDir) } else { - log.Error().Msgf("Failed to parse skin file -- %s. %s. Loading stock skins.", config.K9sStylesFile, err) + log.Error().Msgf("Failed to parse skin file -- %s: %s.", skinFile, err) } - c.updateStyles("") - return + } else { + c.updateStyles(skinFile) } - c.updateStyles(config.K9sStylesFile) } func (c *Configurator) updateStyles(f string) { diff --git a/internal/ui/config_test.go b/internal/ui/config_test.go index 25355d535f..9234131738 100644 --- a/internal/ui/config_test.go +++ b/internal/ui/config_test.go @@ -9,22 +9,49 @@ import ( "testing" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" + "k8s.io/cli-runtime/pkg/genericclioptions" ) func TestBenchConfig(t *testing.T) { - os.Setenv(config.K9sConfig, "/tmp/blee") - assert.Equal(t, "/tmp/blee/bench-fred.yml", ui.BenchConfig("fred")) + os.Setenv(config.K9sConfigDir, "/tmp/test-config") + assert.NoError(t, config.InitLocs()) + defer assert.NoError(t, os.RemoveAll(config.K9sConfigDir)) + + bc, error := config.EnsureBenchmarksCfgFile("cl-1", "ct-1") + assert.NoError(t, error) + assert.Equal(t, "/tmp/test-config/clusters/cl-1/ct-1/benchmarks.yaml", bc) } -func TestConfiguratorRefreshStyle(t *testing.T) { - config.K9sStylesFile = filepath.Join("..", "config", "testdata", "black_and_wtf.yml") +func TestSkinnedContext(t *testing.T) { + os.Setenv(config.K9sConfigDir, "/tmp/test-config") + assert.NoError(t, config.InitLocs()) + defer assert.NoError(t, os.RemoveAll(config.K9sConfigDir)) + + sf := filepath.Join("..", "config", "testdata", "black_and_wtf.yaml") + raw, err := os.ReadFile(sf) + assert.NoError(t, err) + tf := filepath.Join(config.AppSkinsDir, "black_and_wtf.yaml") + assert.NoError(t, os.WriteFile(tf, raw, data.DefaultFileMod)) + + var cfg ui.Configurator + cfg.Config = mock.NewMockConfig() + cl, ct := "cl-1", "ct-1" + flags := genericclioptions.ConfigFlags{ + ClusterName: &cl, + Context: &ct, + } - cfg := ui.Configurator{} - cfg.RefreshStyles("") + cfg.Config.K9s = config.NewK9s( + mock.NewMockConnection(), + mock.NewMockKubeSettings(&flags)) + cfg.Config.K9s.UI = config.UI{Skin: "black_and_wtf"} + cfg.RefreshStyles("ct-1") assert.True(t, cfg.HasSkin()) assert.Equal(t, tcell.ColorGhostWhite.TrueColor(), render.StdColor) diff --git a/internal/ui/crumbs_test.go b/internal/ui/crumbs_test.go index 7d992768db..644f27459f 100644 --- a/internal/ui/crumbs_test.go +++ b/internal/ui/crumbs_test.go @@ -49,11 +49,13 @@ func (c c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return func (c 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 c } -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) 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 c } +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) {} diff --git a/internal/ui/flash.go b/internal/ui/flash.go index 33c9a8c1e4..f188464193 100644 --- a/internal/ui/flash.go +++ b/internal/ui/flash.go @@ -85,7 +85,7 @@ func (f *Flash) SetMessage(m model.LevelMessage) { } func (f *Flash) flashEmoji(l model.FlashLevel) string { - if f.app.Config.K9s.NoIcons { + if f.app.Config.K9s.UI.NoIcons { return "" } // nolint:exhaustive diff --git a/internal/ui/flash_test.go b/internal/ui/flash_test.go index 1b5cd6e3ce..5122152efc 100644 --- a/internal/ui/flash_test.go +++ b/internal/ui/flash_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" @@ -25,7 +25,7 @@ func TestFlash(t *testing.T) { "err": {l: model.FlashErr, i: "hello", e: "😡 hello\n"}, } - a := ui.NewApp(config.NewConfig(nil), "test") + a := ui.NewApp(mock.NewMockConfig(), "test") f := ui.NewFlash(a) f.SetTestMode(true) ctx, cancel := context.WithCancel(context.Background()) diff --git a/internal/ui/indicator_test.go b/internal/ui/indicator_test.go index ad782274d2..40032517e6 100644 --- a/internal/ui/indicator_test.go +++ b/internal/ui/indicator_test.go @@ -7,12 +7,13 @@ import ( "testing" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" ) func TestIndicatorReset(t *testing.T) { - i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles()) + i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(), ""), config.NewStyles()) i.SetPermanent("Blee") i.Info("duh") i.Reset() @@ -21,21 +22,21 @@ func TestIndicatorReset(t *testing.T) { } func TestIndicatorInfo(t *testing.T) { - i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles()) + i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(), ""), config.NewStyles()) i.Info("Blee") assert.Equal(t, "[lawngreen::b] \n", i.GetText(false)) } func TestIndicatorWarn(t *testing.T) { - i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles()) + i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(), ""), config.NewStyles()) i.Warn("Blee") assert.Equal(t, "[mediumvioletred::b] \n", i.GetText(false)) } func TestIndicatorErr(t *testing.T) { - i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles()) + i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(), ""), config.NewStyles()) i.Err("Blee") assert.Equal(t, "[orangered::b] \n", i.GetText(false)) diff --git a/internal/ui/pages.go b/internal/ui/pages.go index 580bb09628..446457156c 100644 --- a/internal/ui/pages.go +++ b/internal/ui/pages.go @@ -102,7 +102,7 @@ func (p *Pages) StackTop(top model.Component) { func componentID(c model.Component) string { if c.Name() == "" { - panic("Component has no name") + log.Error().Msg("Component has no name") } return fmt.Sprintf("%s-%p", c.Name(), c) } diff --git a/internal/ui/prompt_test.go b/internal/ui/prompt_test.go index 1348ca7301..56a6e25025 100644 --- a/internal/ui/prompt_test.go +++ b/internal/ui/prompt_test.go @@ -70,7 +70,10 @@ func TestPromptColor(t *testing.T) { app := ui.App{} // Make sure to have different values to be sure that the prompt color actually changes depending on its type - assert.NotEqual(t, styles.Prompt().Border.DefaultColor.Color(), styles.Prompt().Border.CommandColor.Color()) + assert.NotEqual(t, + styles.Prompt().Border.DefaultColor.Color(), + styles.Prompt().Border.CommandColor.Color(), + ) testCases := []struct { kind model.BufferKind diff --git a/internal/ui/select_table.go b/internal/ui/select_table.go index cf07c9f4d1..25f33c0f19 100644 --- a/internal/ui/select_table.go +++ b/internal/ui/select_table.go @@ -15,7 +15,8 @@ type SelectTable struct { model Tabular selectedFn func(string) string marks map[string]struct{} - fgColor tcell.Color + selFgColor tcell.Color + selBgColor tcell.Color } // SetModel sets the table model. @@ -102,7 +103,7 @@ func (s *SelectTable) GetSelectedRowIndex() int { } // SelectRow select a given row by index. -func (s *SelectTable) SelectRow(r int, broadcast bool) { +func (s *SelectTable) SelectRow(r, c int, broadcast bool) { if !broadcast { s.SetSelectionChangedFunc(nil) } @@ -110,13 +111,13 @@ func (s *SelectTable) SelectRow(r int, broadcast bool) { r = c + 1 } defer s.SetSelectionChangedFunc(s.selectionChanged) - s.Select(r, 0) + s.Select(r, c) } // UpdateSelection refresh selected row. func (s *SelectTable) updateSelection(broadcast bool) { - r, _ := s.GetSelection() - s.SelectRow(r, broadcast) + r, c := s.GetSelection() + s.SelectRow(r, c, broadcast) } func (s *SelectTable) selectionChanged(r, c int) { @@ -124,7 +125,9 @@ func (s *SelectTable) selectionChanged(r, c int) { return } if cell := s.GetCell(r, c); cell != nil { - s.SetSelectedStyle(tcell.StyleDefault.Foreground(s.fgColor).Background(cell.Color).Attributes(tcell.AttrBold)) + s.SetSelectedStyle( + tcell.StyleDefault.Foreground(s.selFgColor). + Background(cell.Color).Attributes(tcell.AttrBold)) } } diff --git a/internal/ui/table.go b/internal/ui/table.go index 47d1714b0b..d2f2f7adb0 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -14,6 +14,7 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/vul" "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/rs/zerolog/log" @@ -98,8 +99,11 @@ func (t *Table) StylesChanged(s *config.Styles) { t.SetBackgroundColor(s.Table().BgColor.Color()) t.SetBorderColor(s.Frame().Border.FgColor.Color()) t.SetBorderFocusColor(s.Frame().Border.FocusColor.Color()) - t.SetSelectedStyle(tcell.StyleDefault.Foreground(t.styles.Table().CursorFgColor.Color()).Background(t.styles.Table().CursorBgColor.Color()).Attributes(tcell.AttrBold)) - t.fgColor = s.Table().CursorFgColor.Color() + t.SetSelectedStyle( + tcell.StyleDefault.Foreground(t.styles.Table().CursorFgColor.Color()). + Background(t.styles.Table().CursorBgColor.Color()).Attributes(tcell.AttrBold)) + t.selFgColor = s.Table().CursorFgColor.Color() + t.selBgColor = s.Table().CursorBgColor.Color() t.Refresh() } @@ -241,6 +245,10 @@ func (t *Table) doUpdate(data *render.TableData) { 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) @@ -286,6 +294,9 @@ func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads M if h[c].MX && !t.hasMetrics { continue } + if h[c].VS && vul.ImgScanner == nil { + continue + } if !re.Deltas.IsBlank() && !h.IsTimeCol(c) { field += Deltas(re.Deltas[c], field) @@ -427,7 +438,7 @@ func (t *Table) UpdateTitle() { } func (t *Table) styleTitle() string { - rc := t.GetRowCount() + rc := int64(t.GetRowCount()) if rc > 0 { rc-- } @@ -451,17 +462,20 @@ func (t *Table) styleTitle() string { } var title string if ns == client.ClusterScope { - title = SkinTitle(fmt.Sprintf(TitleFmt, base, rc), t.styles.Frame()) + title = SkinTitle(fmt.Sprintf(TitleFmt, base, render.AsThousands(rc)), t.styles.Frame()) } else { - title = SkinTitle(fmt.Sprintf(NSTitleFmt, base, ns, rc), t.styles.Frame()) + title = SkinTitle(fmt.Sprintf(NSTitleFmt, base, ns, render.AsThousands(rc)), t.styles.Frame()) } buff := t.cmdBuff.GetText() - if buff == "" { - return title - } if IsLabelSelector(buff) { buff = TrimLabelSelector(buff) + } else if l := t.GetModel().GetLabelFilter(); l != "" { + buff = l + } + + if buff == "" { + return title } return title + SkinTitle(fmt.Sprintf(SearchFmt, buff), t.styles.Frame()) diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go index 5575c94528..85a6e6896b 100644 --- a/internal/ui/table_helper.go +++ b/internal/ui/table_helper.go @@ -24,10 +24,10 @@ const ( SearchFmt = "<[filter:bg:r]/%s[fg:bg:-]> " // NSTitleFmt represents a namespaced view title. - NSTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] " + NSTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] " // TitleFmt represents a standard view title. - TitleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] " + TitleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] " descIndicator = "↓" ascIndicator = "↑" @@ -71,7 +71,12 @@ func IsLabelSelector(s string) bool { if s == "" { return false } - return LabelRx.MatchString(s) + + if LabelRx.MatchString(s) { + return true + } + + return !strings.Contains(s, " ") && strings.Contains(s, "=") } // IsFuzzySelector checks if query is fuzzy. @@ -92,7 +97,11 @@ func IsInverseSelector(s string) bool { // TrimLabelSelector extracts label query. func TrimLabelSelector(s string) string { - return strings.TrimSpace(s[2:]) + if strings.Index(s, "-l") == 0 { + return strings.TrimSpace(s[2:]) + } + + return s } // SkinTitle decorates a title. diff --git a/internal/ui/table_helper_test.go b/internal/ui/table_helper_test.go index f532643ef8..072e72970d 100644 --- a/internal/ui/table_helper_test.go +++ b/internal/ui/table_helper_test.go @@ -11,19 +11,20 @@ import ( func TestIsLabelSelector(t *testing.T) { uu := map[string]struct { - sel string - e bool + s string + ok bool }{ - "cool": {"-l app=fred,env=blee", true}, - "noMode": {"app=fred,env=blee", false}, - "noSpace": {"-lapp=fred,env=blee", true}, - "wrongLabel": {"-f app=fred,env=blee", false}, + "empty": {s: ""}, + "cool": {s: "-l app=fred,env=blee", ok: true}, + "no-flag": {s: "app=fred,env=blee", ok: true}, + "no-space": {s: "-lapp=fred,env=blee", ok: true}, + "wrong-flag": {s: "-f app=fred,env=blee"}, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, IsLabelSelector(u.sel)) + assert.Equal(t, u.ok, IsLabelSelector(u.s)) }) } } diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index ab5e09ba01..2962de9339 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -44,7 +44,7 @@ func TestTableSelection(t *testing.T) { m := &mockModel{} v.SetModel(m) v.Update(m.Peek(), false) - v.SelectRow(1, true) + v.SelectRow(1, 0, true) r, ok := v.GetSelectedRow("r1") assert.True(t, ok) @@ -68,6 +68,7 @@ var _ ui.Tabular = &mockModel{} 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) Count() int { return 1 } func (t *mockModel) HasMetrics() bool { return true } @@ -83,15 +84,12 @@ 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) Delete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error { return nil } - func (t *mockModel) Describe(context.Context, string) (string, error) { return "", nil } - func (t *mockModel) ToYAML(ctx context.Context, path string) (string, error) { return "", nil } diff --git a/internal/ui/types.go b/internal/ui/types.go index b426bc24cf..8013c5e7e8 100644 --- a/internal/ui/types.go +++ b/internal/ui/types.go @@ -57,6 +57,9 @@ type Tabular interface { // SetLabelFilter sets the label filter. SetLabelFilter(string) + // GetLabelFilter fetch the label filter. + GetLabelFilter() string + // Empty returns true if model has no data. Empty() bool diff --git a/internal/view/actions.go b/internal/view/actions.go index 713778175e..324e623318 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -90,11 +90,11 @@ func gotoCmd(r Runner, cmd, path string) ui.ActionHandler { func pluginActions(r Runner, aa ui.KeyActions) { pp := config.NewPlugins() - if err := pp.Load(); err != nil { + if err := pp.Load(r.App().Config.ContextPluginsPath()); err != nil { return } - for k, plugin := range pp.Plugin { + for k, plugin := range pp.Plugins { if !inScope(plugin.Scopes, r.Aliases()) { continue } diff --git a/internal/view/alias.go b/internal/view/alias.go index fc33dc9237..7e654cba4d 100644 --- a/internal/view/alias.go +++ b/internal/view/alias.go @@ -54,7 +54,7 @@ func (a *Alias) bindKeys(aa ui.KeyActions) { tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, true), ui.KeyShiftR: ui.NewKeyAction("Sort Resource", a.GetTable().SortColCmd("RESOURCE", true), false), ui.KeyShiftC: ui.NewKeyAction("Sort Command", a.GetTable().SortColCmd("COMMAND", true), false), - ui.KeyShiftA: ui.NewKeyAction("Sort ApiGroup", a.GetTable().SortColCmd("APIGROUP", true), false), + ui.KeyShiftA: ui.NewKeyAction("Sort ApiGroup", a.GetTable().SortColCmd("API-GROUP", true), false), }) } diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go index 1b82b378bf..15cd393694 100644 --- a/internal/view/alias_test.go +++ b/internal/view/alias_test.go @@ -10,7 +10,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" @@ -18,7 +18,6 @@ import ( "github.com/derailed/k9s/internal/view" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -74,33 +73,11 @@ func (b *buffL) BufferActive(state bool, kind model.BufferKind) { } func makeContext() context.Context { - a := view.NewApp(config.NewConfig(ks{})) + a := view.NewApp(mock.NewMockConfig()) ctx := context.WithValue(context.Background(), internal.KeyApp, a) return context.WithValue(ctx, internal.KeyStyles, a.Styles) } -type ks struct{} - -func (k ks) CurrentContextName() (string, error) { - return "test", nil -} - -func (k ks) CurrentClusterName() (string, error) { - return "test", nil -} - -func (k ks) CurrentNamespaceName() (string, error) { - return "test", nil -} - -func (k ks) ClusterNames() (map[string]struct{}, error) { - return map[string]struct{}{"test": {}}, nil -} - -func (k ks) NamespaceNames(nn []v1.Namespace) []string { - return []string{"test"} -} - type mockModel struct{} var ( @@ -114,6 +91,7 @@ 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) Count() int { return 1 } func (t *mockModel) HasMetrics() bool { return true } diff --git a/internal/view/app.go b/internal/view/app.go index 374f0ba74c..9cbf742dd2 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -23,12 +23,12 @@ import ( "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" + "github.com/derailed/k9s/internal/view/cmd" "github.com/derailed/k9s/internal/vul" "github.com/derailed/k9s/internal/watch" "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/rs/zerolog/log" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // ExitStatus indicates UI exit conditions. @@ -61,7 +61,7 @@ type App struct { // NewApp returns a K9s app instance. func NewApp(cfg *config.Config) *App { a := App{ - App: ui.NewApp(cfg, cfg.K9s.CurrentContext), + App: ui.NewApp(cfg, cfg.K9s.ActiveContextName()), cmdHistory: model.NewHistory(model.MaxHistory), filterHistory: model.NewHistory(model.MaxHistory), Content: NewPageStack(), @@ -113,7 +113,7 @@ func (a *App) Init(version string, rate int) error { } a.command = NewCommand(a) - if err := a.command.Init(); err != nil { + if err := a.command.Init(a.Config.ContextAliasesPath()); err != nil { return err } a.CmdBuff().SetSuggestionFn(a.suggestCommand()) @@ -121,7 +121,7 @@ func (a *App) Init(version string, rate int) error { a.layout(ctx) a.initSignals() - if a.Config.K9s.EnableImageScan { + if a.Config.K9s.ImageScans.Enable { a.initImgScanner(version) } @@ -139,7 +139,7 @@ func (a *App) initImgScanner(version string) { log.Debug().Msgf("Scanner init time %s", time.Since(t)) }(time.Now()) - vul.ImgScanner = vul.NewImageScanner() + vul.ImgScanner = vul.NewImageScanner(a.Config.K9s.ImageScans) go vul.ImgScanner.Init("k9s", version) } @@ -186,39 +186,30 @@ func (a *App) suggestCommand() model.SuggestionFunc { s = strings.ToLower(s) for _, k := range a.command.alias.Aliases.Keys() { - if suggest, ok := shouldAddSuggest(s, k); ok { + if suggest, ok := cmd.ShouldAddSuggest(s, k); ok { entries = append(entries, suggest) } } - namespaceNames, err := a.namespaceNames() + namespaceNames, err := a.factory.Client().ValidNamespaceNames() if err != nil { log.Error().Err(err).Msg("failed to list namespaces") } - entries = append(entries, suggestSubCommand(s, namespaceNames, contextNames)...) + entries = append(entries, cmd.SuggestSubCommand(s, namespaceNames, contextNames)...) if len(entries) == 0 { return nil } + entries.Sort() return } } -func (a *App) namespaceNames() ([]string, error) { - namespaces, err := a.factory.Client().ValidNamespaces() - if err != nil { - return nil, err - } - - namespaceNames := make([]string, 0, len(namespaces)) - for _, namespace := range namespaces { - namespaceNames = append(namespaceNames, namespace.Name) - } - return namespaceNames, nil -} - func (a *App) contextNames() ([]string, error) { + if !a.Conn().ConnectionOK() { + return nil, errors.New("no connection") + } contexts, err := a.factory.Client().Config().Contexts() if err != nil { return nil, err @@ -302,11 +293,13 @@ func (a *App) buildHeader() tview.Primitive { } clWidth := clusterInfoWidth - n, err := a.Conn().Config().CurrentClusterName() - if err == nil { - size := len(n) + clusterInfoPad - if size > clWidth { - clWidth = size + if a.Conn().ConnectionOK() { + n, err := a.Conn().Config().CurrentClusterName() + if err == nil { + size := len(n) + clusterInfoPad + if size > clWidth { + clWidth = size + } } } header.AddItem(a.clusterInfo(), clWidth, 1, false) @@ -338,6 +331,8 @@ func (a *App) Resume() { } if err := a.CustomViewsWatcher(ctx, a); err != nil { log.Warn().Err(err).Msgf("CustomView watcher failed") + } else { + log.Debug().Msgf("CustomViews watching `%s", config.AppViewsFile) } } @@ -400,7 +395,7 @@ func (a *App) refreshCluster(context.Context) error { // Reload alias go func() { - if err := a.command.Reset(false); err != nil { + if err := a.command.Reset(a.Config.ContextAliasesPath(), false); err != nil { log.Error().Err(err).Msgf("Command reset failed") } }() @@ -413,12 +408,8 @@ func (a *App) refreshCluster(context.Context) error { func (a *App) switchNS(ns string) error { if ns == client.ClusterScope { - ns = client.AllNamespaces - } - if ns == a.Config.ActiveNamespace() { - return nil + ns = client.BlankNamespace } - ok, err := a.isValidNS(ns) if err != nil { return err @@ -437,56 +428,51 @@ func (a *App) switchNS(ns string) error { } func (a *App) isValidNS(ns string) (bool, error) { - if ns == client.AllNamespaces || ns == client.NamespaceAll { + if ns == client.BlankNamespace || ns == client.NamespaceAll { return true, nil } - ctx, cancel := context.WithTimeout(context.Background(), a.Conn().Config().CallTimeout()) - defer cancel() - dial, err := a.Conn().Dial() - if err != nil { - return false, err - } - _, err = dial.CoreV1().Namespaces().Get(ctx, ns, metav1.GetOptions{}) - if err != nil { - log.Warn().Err(err).Msgf("Validation failed for namespace: %q", ns) + if !a.Conn().IsValidNamespace(ns) { + return false, fmt.Errorf("invalid namespace: %q", ns) } return true, nil } -func (a *App) switchContext(name string) error { +func (a *App) switchContext(ci *cmd.Interpreter) error { + name, ok := ci.HasContext() + if !ok { + return nil + } + log.Debug().Msgf("--> Switching Context %q--%q", name, a.Config.ActiveView()) a.Halt() defer a.Resume() { - ns, err := a.Conn().Config().CurrentNamespaceName() - if err != nil { - log.Warn().Msg("No namespace specified in context. Using K9s config") - ns = a.Config.ActiveNamespace() - } - a.initFactory(ns) - - if e := a.command.Reset(true); e != nil { - return e - } - if a.Config.ActiveView() == "" || isContextCmd(a.Config.ActiveView()) { + p := cmd.NewInterpreter(a.Config.ActiveView()) + if p.IsContextCmd() { a.Config.SetActiveView("pod") } + p.ResetContextArg() + a.Config.Reset() - a.Config.K9s.CurrentContext = name - cluster, err := a.Conn().Config().CurrentClusterName() + ct, err := a.Config.K9s.ActivateContext(name) if err != nil { return err } - a.Config.K9s.CurrentCluster = cluster - if err := a.Config.SetActiveNamespace(ns); err != nil { - log.Error().Err(err).Msg("unable to set active ns") + if err := a.command.Reset(a.Config.ContextAliasesPath(), true); err != nil { + return err + } + if cns, ok := ci.NSArg(); ok { + ct.Namespace.Active = cns } if err := a.Config.Save(); err != nil { log.Error().Err(err).Msg("config save failed!") } + ns := a.Config.ActiveNamespace() + a.initFactory(ns) + a.Flash().Infof("Switching context to %s", name) a.ReloadStyles(name) a.gotoResource(a.Config.ActiveView(), "", true) @@ -682,10 +668,6 @@ func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey { } func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.CmdBuff().InCmdMode() { - return evt - } - if a.Content.Top() != nil && a.Content.Top().Name() == aliasTitle { a.Content.Pop() return nil @@ -698,8 +680,8 @@ func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (a *App) gotoResource(cmd, path string, clearStack bool) { - err := a.command.run(cmd, path, clearStack) +func (a *App) gotoResource(c, path string, clearStack bool) { + err := a.command.run(cmd.NewInterpreter(c), path, clearStack) if err != nil { dialog.ShowError(a.Styles.Dialog(), a.Content.Pages, err.Error()) } @@ -727,45 +709,3 @@ func (a *App) clusterInfo() *ClusterInfo { func (a *App) statusIndicator() *ui.StatusIndicator { return a.Views()["statusIndicator"].(*ui.StatusIndicator) } - -// ---------------------------------------------------------------------------- -// Helpers - -func suggestSubCommand(command string, namespaces, contexts []string) []string { - cmds := strings.Fields(command) - if len(cmds[0]) == 0 || len(cmds) != 2 { - return nil - } - - var suggests []string - switch strings.ToLower(cmds[0]) { - case "cow", "q", "q!", "qa", "Q", "quit", "?", "h", "help", "a", "alias", "x", "xray", "dir": - return nil // ignore special commands - case "ctx", "context", "contexts": - for _, ctxName := range contexts { - if suggest, ok := shouldAddSuggest(cmds[1], ctxName); ok { - suggests = append(suggests, suggest) - } - } - default: - if suggest, ok := shouldAddSuggest(cmds[1], client.NamespaceAll); ok { - suggests = append(suggests, suggest) - } - - for _, ns := range namespaces { - if suggest, ok := shouldAddSuggest(cmds[1], ns); ok { - suggests = append(suggests, suggest) - } - } - } - - return suggests -} - -func shouldAddSuggest(command, suggest string) (string, bool) { - if command != suggest && strings.HasPrefix(suggest, command) { - return strings.TrimPrefix(suggest, command), true - } - - return "", false -} diff --git a/internal/view/app_int_test.go b/internal/view/app_int_test.go deleted file mode 100644 index f20a26c098..0000000000 --- a/internal/view/app_int_test.go +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package view - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_suggestSubCommand(t *testing.T) { - namespaceNames := []string{"kube-system", "kube-public", "default", "nginx-ingress"} - contextNames := []string{"develop", "test", "pre", "prod"} - - tests := []struct { - Command string - Suggestions []string - }{ - {Command: "q", Suggestions: nil}, - {Command: "xray dp", Suggestions: nil}, - {Command: "help k", Suggestions: nil}, - {Command: "ctx p", Suggestions: []string{"re", "rod"}}, - {Command: "ctx p", Suggestions: []string{"re", "rod"}}, - {Command: "ctx pr", Suggestions: []string{"e", "od"}}, - {Command: "context d", Suggestions: []string{"evelop"}}, - {Command: "contexts t", Suggestions: []string{"est"}}, - {Command: "po ", Suggestions: nil}, - {Command: "po x", Suggestions: nil}, - {Command: "po k", Suggestions: []string{"ube-system", "ube-public"}}, - {Command: "po kube-", Suggestions: []string{"system", "public"}}, - } - - for _, tt := range tests { - got := suggestSubCommand(tt.Command, namespaceNames, contextNames) - assert.Equal(t, tt.Suggestions, got) - } -} diff --git a/internal/view/app_test.go b/internal/view/app_test.go index e214d7c4db..924fb8f2cc 100644 --- a/internal/view/app_test.go +++ b/internal/view/app_test.go @@ -6,13 +6,13 @@ package view_test import ( "testing" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" ) func TestAppNew(t *testing.T) { - a := view.NewApp(config.NewConfig(ks{})) + a := view.NewApp(mock.NewMockConfig()) _ = a.Init("blee", 10) assert.Equal(t, 11, len(a.GetActions())) diff --git a/internal/view/benchmark.go b/internal/view/benchmark.go index daa26be302..8a5232eca5 100644 --- a/internal/view/benchmark.go +++ b/internal/view/benchmark.go @@ -12,9 +12,9 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/perf" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" + "github.com/rs/zerolog/log" ) // Benchmark represents a service benchmark results view. @@ -40,7 +40,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, path string) { +func (b *Benchmark) viewBench(app *App, model ui.Tabular, gvr client.GVR, path string) { data, err := readBenchFile(app.Config, b.benchFile()) if err != nil { app.Flash().Errf("Unable to load bench file %s", err) @@ -68,7 +68,15 @@ func fileToSubject(path string) string { } func benchDir(cfg *config.Config) string { - return filepath.Join(perf.K9sBenchDir, cfg.K9s.CurrentCluster) + ct, err := cfg.K9s.ActiveContext() + if err != nil { + log.Error().Err(err).Msgf("no active context located") + } + return filepath.Join( + config.AppBenchmarksDir, + config.SanitizeFileName(ct.ClusterName), + config.SanitizeFilename(cfg.K9s.ActiveContextName()), + ) } func readBenchFile(cfg *config.Config, n string) (string, error) { diff --git a/internal/view/browser.go b/internal/view/browser.go index 240f4dffb0..429e3e94bb 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -14,7 +14,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" @@ -166,6 +166,15 @@ func (b *Browser) Stop() { b.Table.Stop() } +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)) +} + // BufferChanged indicates the buffer was changed. func (b *Browser) BufferChanged(_, _ string) {} @@ -280,7 +289,9 @@ func (b *Browser) helpCmd(evt *tcell.EventKey) *tcell.EventKey { func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey { if !b.CmdBuff().InCmdMode() { b.CmdBuff().ClearText(false) + b.GetModel().SetLabelFilter("") return b.App().PrevCmd(evt) + } b.CmdBuff().Reset() @@ -317,7 +328,7 @@ func (b *Browser) enterCmd(evt *tcell.EventKey) *tcell.EventKey { if b.enterFn != nil { f = b.enterFn } - f(b.app, b.GetModel(), b.GVR().String(), path) + f(b.app, b.GetModel(), b.GVR(), path) return nil } @@ -357,7 +368,7 @@ func (b *Browser) describeCmd(evt *tcell.EventKey) *tcell.EventKey { if path == "" { return evt } - describeResource(b.app, b.GetModel(), b.GVR().String(), path) + describeResource(b.app, b.GetModel(), b.GVR(), path) return nil } @@ -383,7 +394,7 @@ func editRes(app *App, gvr client.GVR, path string) error { } ns, n := client.Namespaced(path) if client.IsClusterScoped(ns) { - ns = client.AllNamespaces + ns = client.BlankNamespace } if gvr.String() == "v1/namespaces" { ns = n @@ -395,7 +406,7 @@ func editRes(app *App, gvr client.GVR, path string) error { args := make([]string, 0, 10) args = append(args, "edit") args = append(args, gvr.FQN(n)) - if ns != client.AllNamespaces { + if ns != client.BlankNamespace { args = append(args, "-n", ns) } if err := runK(app, shellOpts{clear: true, args: args}); err != nil { @@ -434,7 +445,7 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { b.app.Flash().Infof("Viewing namespace `%s`...", ns) b.refresh() b.UpdateTitle() - b.SelectRow(1, true) + b.SelectRow(1, 0, true) b.app.CmdBuff().Reset() if err := b.app.Config.SetActiveNamespace(b.GetModel().GetNamespace()); err != nil { log.Error().Err(err).Msg("Config save NS failed!") @@ -462,14 +473,13 @@ func (b *Browser) setNamespace(ns string) { func (b *Browser) defaultContext() context.Context { ctx := context.WithValue(context.Background(), internal.KeyFactory, b.app.factory) - ctx = context.WithValue(ctx, internal.KeyGVR, b.GVR().String()) - if b.Path != "" { - ctx = context.WithValue(ctx, internal.KeyPath, b.Path) - } + ctx = context.WithValue(ctx, internal.KeyGVR, b.GVR()) + ctx = context.WithValue(ctx, internal.KeyPath, b.Path) if ui.IsLabelSelector(b.CmdBuff().GetText()) { ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(b.CmdBuff().GetText())) } ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(b.App().Config.ActiveNamespace())) + ctx = context.WithValue(ctx, internal.KeyWithMetrics, b.app.factory.Client().HasMetrics()) return ctx } @@ -514,7 +524,7 @@ func (b *Browser) namespaceActions(aa ui.KeyActions) { if !b.meta.Namespaced || b.GetTable().Path != "" { return } - b.namespaces = make(map[int]string, config.MaxFavoritesNS) + b.namespaces = make(map[int]string, data.MaxFavoritesNS) aa[ui.Key0] = ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true) b.namespaces[0] = client.NamespaceAll index := 1 diff --git a/internal/view/cm.go b/internal/view/cm.go index c1694ca6dc..05c36da2a1 100644 --- a/internal/view/cm.go +++ b/internal/view/cm.go @@ -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(), "v1/configmaps") + return scanRefs(evt, s.App(), s.GetTable(), dao.CmGVR) } -func scanRefs(evt *tcell.EventKey, a *App, t *Table, gvr string) *tcell.EventKey { +func scanRefs(evt *tcell.EventKey, a *App, t *Table, gvr client.GVR) *tcell.EventKey { path := t.GetSelectedItem() if path == "" { return evt @@ -64,7 +64,7 @@ func scanRefs(evt *tcell.EventKey, a *App, t *Table, gvr string) *tcell.EventKey return nil } -func refContext(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/cmd/args.go b/internal/view/cmd/args.go new file mode 100644 index 0000000000..73afbffbb2 --- /dev/null +++ b/internal/view/cmd/args.go @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd + +import ( + "strings" +) + +const ( + nsKey = "ns" + topicKey = "topic" + filterKey = "filter" + labelKey = "labels" + contextKey = "context" +) + +type args map[string]string + +func newArgs(p *Interpreter, aa []string) args { + args := make(args, len(aa)) + if len(aa) == 0 { + return args + } + + for i := 0; i < len(aa); i++ { + a := strings.TrimSpace(aa[i]) + switch { + case strings.Index(a, contextFlag) == 0: + args[contextKey] = a[1:] + + case strings.Index(a, fuzzyFlag) == 0: + i++ + args[filterKey] = strings.TrimSpace(aa[i]) + continue + + case strings.Index(a, filterFlag) == 0: + args[filterKey] = a[1:] + + case strings.Contains(a, labelFlag): + if ll := toLabels(a); len(ll) != 0 { + args[labelKey] = a + } + + default: + a := strings.TrimSpace(aa[i]) + switch { + case p.IsContextCmd(): + args[contextKey] = a + case p.IsDirCmd(): + if _, ok := args[topicKey]; !ok { + args[topicKey] = a + } + case p.IsXrayCmd(): + if _, ok := args[topicKey]; ok { + args[nsKey] = a + } else { + args[topicKey] = a + } + default: + args[nsKey] = a + } + } + } + + return args +} + +func (a args) hasFilters() bool { + _, fok := a[filterKey] + _, lok := a[labelKey] + + return fok || lok +} diff --git a/internal/view/cmd/args_test.go b/internal/view/cmd/args_test.go new file mode 100644 index 0000000000..df4e4c319b --- /dev/null +++ b/internal/view/cmd/args_test.go @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFlagsNew(t *testing.T) { + uu := map[string]struct { + i *Interpreter + aa []string + ll args + }{ + "empty": { + i: NewInterpreter("po"), + ll: make(args), + }, + "ns": { + i: NewInterpreter("po"), + aa: []string{"ns1"}, + ll: args{nsKey: "ns1"}, + }, + "ns+spaces": { + i: NewInterpreter("po"), + aa: []string{" ns1 "}, + ll: args{nsKey: "ns1"}, + }, + "filter": { + i: NewInterpreter("po"), + aa: []string{"/fred"}, + ll: args{filterKey: "fred"}, + }, + "inverse-filter": { + i: NewInterpreter("po"), + aa: []string{"/!fred"}, + ll: args{filterKey: "!fred"}, + }, + "fuzzy-filter": { + i: NewInterpreter("po"), + aa: []string{"-f", "fred"}, + ll: args{filterKey: "fred"}, + }, + "filter+ns": { + i: NewInterpreter("po"), + aa: []string{"/fred", " ns1 "}, + ll: args{nsKey: "ns1", filterKey: "fred"}, + }, + "label": { + i: NewInterpreter("po"), + aa: []string{"app=fred"}, + ll: args{labelKey: "app=fred"}, + }, + "label-toast": { + i: NewInterpreter("po"), + aa: []string{"="}, + ll: make(args), + }, + "multi-labels": { + i: NewInterpreter("po"), + aa: []string{"app=fred,blee=duh"}, + ll: args{labelKey: "app=fred,blee=duh"}, + }, + "label+ns": { + i: NewInterpreter("po"), + aa: []string{"a=b,c=d", " ns1 "}, + ll: args{labelKey: "a=b,c=d", nsKey: "ns1"}, + }, + "full-monty": { + i: NewInterpreter("po"), + aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg"}, + ll: args{filterKey: "zorg", labelKey: "app=fred", nsKey: "ns1"}, + }, + "full-monty+ctx": { + i: NewInterpreter("po"), + aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg", "@ctx1"}, + ll: args{filterKey: "zorg", labelKey: "app=fred", nsKey: "ns1", contextKey: "ctx1"}, + }, + } + + for k := range uu { + 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) + }) + } +} + +func TestFlagsHasFilters(t *testing.T) { + uu := map[string]struct { + i *Interpreter + aa []string + ok bool + }{ + "empty": {}, + "ns": { + i: NewInterpreter("po"), + aa: []string{"ns1"}, + }, + "filter": { + i: NewInterpreter("po"), + aa: []string{"/fred"}, + ok: true, + }, + "inverse-filter": { + i: NewInterpreter("po"), + aa: []string{"/!fred"}, + ok: true, + }, + "fuzzy-filter": { + i: NewInterpreter("po"), + aa: []string{"-f", "fred"}, + ok: true, + }, + "filter+ns": { + i: NewInterpreter("po"), + aa: []string{"/fred", "ns1"}, + ok: true, + }, + "label": { + i: NewInterpreter("po"), + aa: []string{"app=fred"}, + ok: true, + }, + "multi-labels": { + i: NewInterpreter("po"), + aa: []string{"app=fred,blee=duh"}, + ok: true, + }, + "label+ns": { + i: NewInterpreter("po"), + aa: []string{"app=fred", "ns1"}, + ok: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + l := newArgs(u.i, u.aa) + assert.Equal(t, u.ok, l.hasFilters()) + }) + } +} diff --git a/internal/view/cmd/helpers.go b/internal/view/cmd/helpers.go new file mode 100644 index 0000000000..5d53d63398 --- /dev/null +++ b/internal/view/cmd/helpers.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd + +import ( + "slices" + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/rs/zerolog/log" +) + +func toLabels(s string) map[string]string { + ll := strings.Split(s, ",") + lbls := make(map[string]string, len(ll)) + for _, l := range ll { + kv := strings.Split(l, "=") + if len(kv) < 2 || kv[0] == "" || kv[1] == "" { + continue + } + lbls[kv[0]] = kv[1] + } + + if len(lbls) == 0 { + return nil + } + + return lbls +} + +// ShouldAddSuggest checks if a suggestion match the given command. +func ShouldAddSuggest(command, suggest string) (string, bool) { + if command != suggest && strings.HasPrefix(suggest, command) { + return strings.TrimPrefix(suggest, command), true + } + + return "", false +} + +// SuggestSubCommand suggests namespaces or contexts based on current command. +func SuggestSubCommand(command string, namespaces client.NamespaceNames, contexts []string) []string { + 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(): + return nil + + case p.IsXrayCmd(): + _, ns, ok := p.XrayArgs() + if !ok || ns == "" { + return nil + } + suggests = completeNS(ns, namespaces) + + case p.IsContextCmd(): + n, ok := p.ContextArg() + if !ok { + return nil + } + suggests = completeCtx(n, contexts) + + case p.HasNS(): + if n, ok := p.HasContext(); ok { + suggests = completeCtx(n, contexts) + } + log.Debug().Msgf("!!SUGG CTX!! %#v", suggests) + if len(suggests) > 0 { + break + } + + ns, ok := p.NSArg() + if !ok { + return nil + } + suggests = completeNS(ns, namespaces) + log.Debug().Msgf("!!SUGG NS!! %#v", suggests) + + default: + if n, ok := p.HasContext(); ok { + suggests = completeCtx(n, contexts) + } + } + slices.Sort(suggests) + + return suggests +} + +func completeNS(s string, nn client.NamespaceNames) []string { + var suggests []string + if suggest, ok := ShouldAddSuggest(s, client.NamespaceAll); ok { + suggests = append(suggests, suggest) + } + for ns := range nn { + if suggest, ok := ShouldAddSuggest(s, ns); ok { + suggests = append(suggests, suggest) + } + } + + return suggests +} + +func completeCtx(s string, cc []string) []string { + var suggests []string + for _, ctxName := range cc { + if suggest, ok := ShouldAddSuggest(s, ctxName); ok { + suggests = append(suggests, suggest) + } + } + + return suggests +} diff --git a/internal/view/cmd/helpers_test.go b/internal/view/cmd/helpers_test.go new file mode 100644 index 0000000000..9b9f5dc406 --- /dev/null +++ b/internal/view/cmd/helpers_test.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd + +import ( + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func Test_toLabels(t *testing.T) { + uu := map[string]struct { + s string + ll map[string]string + }{ + "empty": {}, + "toast": { + s: "=", + }, + "toast-1": { + s: "=,", + }, + "toast-2": { + s: ",", + }, + "toast-3": { + s: ",=", + }, + "simple": { + s: "a=b", + ll: map[string]string{"a": "b"}, + }, + "multi": { + s: "a=b,c=d", + ll: map[string]string{"a": "b", "c": "d"}, + }, + "multi-toast1": { + s: "a=,c=d", + ll: map[string]string{"c": "d"}, + }, + "multi-toast2": { + s: "a=b,=d", + ll: map[string]string{"a": "b"}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.ll, toLabels(u.s)) + }) + } +} + +func TestSuggestSubCommand(t *testing.T) { + namespaceNames := map[string]struct{}{ + "kube-system": {}, + "kube-public": {}, + "default": {}, + "nginx-ingress": {}, + } + contextNames := []string{"develop", "test", "pre", "prod"} + + tests := []struct { + Command string + Suggestions []string + }{ + {Command: "q", Suggestions: nil}, + {Command: "xray dp", Suggestions: nil}, + {Command: "help k", Suggestions: nil}, + {Command: "ctx p", Suggestions: []string{"re", "rod"}}, + {Command: "ctx p", Suggestions: []string{"re", "rod"}}, + {Command: "ctx pr", Suggestions: []string{"e", "od"}}, + {Command: "context d", Suggestions: []string{"evelop"}}, + {Command: "contexts t", Suggestions: []string{"est"}}, + {Command: "po ", Suggestions: nil}, + {Command: "po x", Suggestions: nil}, + {Command: "po k", Suggestions: []string{"ube-public", "ube-system"}}, + {Command: "po kube-", Suggestions: []string{"public", "system"}}, + } + + for _, tt := range tests { + got := SuggestSubCommand(tt.Command, namespaceNames, contextNames) + assert.Equal(t, tt.Suggestions, got) + } +} diff --git a/internal/view/cmd/interpreter.go b/internal/view/cmd/interpreter.go new file mode 100644 index 0000000000..fce8980ebd --- /dev/null +++ b/internal/view/cmd/interpreter.go @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd + +import ( + "strings" +) + +type Interpreter struct { + line string + cmd string + args args +} + +func NewInterpreter(s string) *Interpreter { + c := Interpreter{ + line: strings.ToLower(s), + args: make(args), + } + c.grok() + + return &c +} + +func (c *Interpreter) grok() { + ff := strings.Fields(c.line) + if len(ff) == 0 { + return + } + c.cmd = ff[0] + c.args = newArgs(c, ff[1:]) +} + +func (c *Interpreter) HasNS() bool { + ns, ok := c.args[nsKey] + + return ok && ns != "" +} + +func (c *Interpreter) Cmd() string { + return c.cmd +} + +func (c *Interpreter) IsBlank() bool { + return c.line == "" +} + +func (c *Interpreter) Amend(c1 *Interpreter) { + c.cmd = c1.cmd + if c.args == nil { + c.args = make(args, len(c1.args)) + } + for k, v := range c1.args { + if v != "" { + c.args[k] = v + } + } +} + +func (c *Interpreter) Reset(s string) *Interpreter { + c.line = strings.ToLower(s) + c.grok() + + return c +} + +func (c *Interpreter) GetLine() string { + return strings.TrimSpace(c.line) +} + +func (c *Interpreter) IsCowCmd() bool { + return c.cmd == cowCmd +} + +func (c *Interpreter) IsHelpCmd() bool { + _, ok := helpCmd[c.cmd] + return ok +} + +func (c *Interpreter) IsBailCmd() bool { + _, ok := bailCmd[c.cmd] + return ok +} + +func (c *Interpreter) IsAliasCmd() bool { + _, ok := aliasCmd[c.cmd] + return ok +} + +func (c *Interpreter) IsXrayCmd() bool { + _, ok := xrayCmd[c.cmd] + + return ok +} + +func (c *Interpreter) IsContextCmd() bool { + _, ok := contextCmd[c.cmd] + + return ok +} + +func (c *Interpreter) IsDirCmd() bool { + _, ok := dirCmd[c.cmd] + return ok +} + +func (c *Interpreter) IsRBACCmd() bool { + return c.cmd == canCmd +} + +func (c *Interpreter) ContextArg() (string, bool) { + if !c.IsContextCmd() { + return "", false + } + + return c.args[contextKey], true +} + +func (c *Interpreter) ResetContextArg() { + delete(c.args, contextFlag) +} + +func (c *Interpreter) DirArg() (string, bool) { + if !c.IsDirCmd() || c.args[topicKey] == "" { + return "", false + } + + return c.args[topicKey], true +} + +func (c *Interpreter) CowArg() (string, bool) { + if !c.IsCowCmd() || c.args[nsKey] == "" { + return "", false + } + + return c.args[nsKey], true +} + +func (c *Interpreter) RBACArgs() (string, string, bool) { + if !c.IsRBACCmd() { + return "", "", false + } + tt := rbacRX.FindStringSubmatch(c.line) + if len(tt) < 3 { + return "", "", false + } + + return tt[1], tt[2], true +} + +func (c *Interpreter) XrayArgs() (string, string, bool) { + if !c.IsXrayCmd() { + return "", "", false + } + gvr, ok1 := c.args[topicKey] + if !ok1 { + return "", "", false + } + + ns, ok2 := c.args[nsKey] + switch { + case ok1 && ok2: + return gvr, ns, true + case ok1 && !ok2: + return gvr, "", true + default: + return "", "", false + } +} + +func (c *Interpreter) FilterArg() (string, bool) { + f, ok := c.args[filterKey] + + return f, ok +} + +func (c *Interpreter) NSArg() (string, bool) { + ns, ok := c.args[nsKey] + + return ns, ok +} + +func (c *Interpreter) HasContext() (string, bool) { + ctx, ok := c.args[contextKey] + if !ok || ctx == "" { + return "", false + } + + return ctx, ok +} + +func (c *Interpreter) LabelsArg() (map[string]string, bool) { + ll, ok := c.args[labelKey] + if !ok { + return nil, false + } + + return toLabels(ll), true +} diff --git a/internal/view/cmd/interpreter_test.go b/internal/view/cmd/interpreter_test.go new file mode 100644 index 0000000000..bb78da78c0 --- /dev/null +++ b/internal/view/cmd/interpreter_test.go @@ -0,0 +1,465 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/view/cmd" + "github.com/stretchr/testify/assert" +) + +func TestRbacCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + args []string + }{ + "empty": {}, + "user": { + cmd: "can u:fernand", + ok: true, + args: []string{"u", "fernand"}, + }, + "user_spacing": { + cmd: "can u: fernand ", + ok: true, + args: []string{"u", "fernand"}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + assert.Equal(t, u.ok, p.IsRBACCmd()) + + c, s, ok := p.RBACArgs() + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.args[0], c) + assert.Equal(t, u.args[1], s) + } + }) + } +} + +func TestNsCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + ns string + }{ + "empty": {}, + "happy": { + cmd: "pod fred", + ok: true, + ns: "fred", + }, + "ns-arg-spaced": { + cmd: "pod fred ", + ok: true, + ns: "fred", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + ns, ok := p.NSArg() + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.ns, ns) + } + }) + } +} + +func TestFilterCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + filter string + }{ + "empty": {}, + "normal": { + cmd: "pod /fred", + ok: true, + filter: "fred", + }, + "caps": { + cmd: "POD /FRED", + ok: true, + filter: "fred", + }, + "filter+ns": { + cmd: "pod /fred ns1", + ok: true, + filter: "fred", + }, + "ns+filter": { + cmd: "pod ns1 /fred", + ok: true, + filter: "fred", + }, + "ns+filter+labels": { + cmd: "pod ns1 /fred app=blee,fred=zorg", + ok: true, + filter: "fred", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + f, ok := p.FilterArg() + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.filter, f) + } + }) + } +} + +func TestLabelCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + labels map[string]string + }{ + "empty": {}, + "plain": { + cmd: "pod fred=blee", + ok: true, + labels: map[string]string{"fred": "blee"}, + }, + "multi": { + cmd: "pod fred=blee,zorg=duh", + ok: true, + labels: map[string]string{"fred": "blee", "zorg": "duh"}, + }, + "multi-ns": { + cmd: "pod fred=blee,zorg=duh ns1", + ok: true, + labels: map[string]string{"fred": "blee", "zorg": "duh"}, + }, + "l-arg-spaced": { + cmd: "pod fred=blee ", + ok: true, + labels: map[string]string{"fred": "blee"}, + }, + "l-arg-caps": { + cmd: "POD FRED=BLEE ", + ok: true, + labels: map[string]string{"fred": "blee"}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + ll, ok := p.LabelsArg() + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.labels, ll) + } + }) + } +} + +func TestXRayCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + res, ns string + }{ + "empty": {}, + + "happy": { + cmd: "xray po", + ok: true, + res: "po", + }, + + "happy+ns": { + cmd: "xray po ns1", + ok: true, + res: "po", + ns: "ns1", + }, + + "toast": { + cmd: "xrayzor po", + }, + + "toast-1": { + cmd: "xray", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + res, ns, ok := p.XrayArgs() + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.res, res) + assert.Equal(t, u.ns, ns) + } + }) + } +} + +func TestDirCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + dir string + }{ + "empty": {}, + + "happy": { + cmd: "dir dir1", + ok: true, + dir: "dir1", + }, + + "extra-ns": { + cmd: "dir dir1 ns1", + ok: true, + dir: "dir1", + }, + + "toast": { + cmd: "dirdel dir1", + }, + + "toast-nodir": { + cmd: "dir", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + dir, ok := p.DirArg() + assert.Equal(t, u.ok, ok) + assert.Equal(t, u.dir, dir) + }) + } +} + +func TestRBACCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + cat, sub string + }{ + "empty": {}, + "toast": { + cmd: "canopy u:bozo", + }, + "toast-1": { + cmd: "can u:", + }, + "toast-2": { + cmd: "can bozo", + }, + "user": { + cmd: "can u:bozo", + ok: true, + cat: "u", + sub: "bozo", + }, + "group": { + cmd: "can g:bozo", + ok: true, + cat: "g", + sub: "bozo", + }, + "sa": { + cmd: "can s:bozo", + ok: true, + cat: "s", + sub: "bozo", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + cat, sub, ok := p.RBACArgs() + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.cat, cat) + assert.Equal(t, u.sub, sub) + } + }) + } +} + +func TestContextCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + ctx string + }{ + "empty": {}, + "plain": { + cmd: "context", + ok: true, + ctx: "", + }, + "happy-full": { + cmd: "context ctx1", + ok: true, + ctx: "ctx1", + }, + "happy-alias": { + cmd: "ctx ctx1", + ok: true, + ctx: "ctx1", + }, + "toast": { + cmd: "ctxto ctx1", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + assert.Equal(t, u.ok, p.IsContextCmd()) + if u.ok { + ctx, ok := p.ContextArg() + assert.Equal(t, u.ok, ok) + assert.Equal(t, u.ctx, ctx) + } + }) + } +} + +func TestHelpCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + }{ + "empty": {}, + "plain": { + cmd: "help", + ok: true, + }, + "toast": { + cmd: "helpme", + }, + "toast1": { + cmd: "hozer", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + assert.Equal(t, u.ok, p.IsHelpCmd()) + }) + } +} + +func TestBailCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + }{ + "empty": {}, + "plain": { + cmd: "quit", + ok: true, + }, + "q": { + cmd: "q", + ok: true, + }, + "q!": { + cmd: "q!", + ok: true, + }, + "toast": { + cmd: "zorg", + }, + "toast1": { + cmd: "quitter", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + assert.Equal(t, u.ok, p.IsBailCmd()) + }) + } +} + +func TestAliasCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + }{ + "empty": {}, + "plain": { + cmd: "alias", + ok: true, + }, + "a": { + cmd: "a", + ok: true, + }, + "toast": { + cmd: "abba", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + assert.Equal(t, u.ok, p.IsAliasCmd()) + }) + } +} + +func TestCowCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + }{ + "empty": {}, + "plain": { + cmd: "cow", + ok: true, + }, + "msg": { + cmd: "cow bumblebeetuna", + ok: true, + }, + "toast": { + cmd: "cowdy", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + assert.Equal(t, u.ok, p.IsCowCmd()) + }) + } +} diff --git a/internal/view/cmd/types.go b/internal/view/cmd/types.go new file mode 100644 index 0000000000..122a3ab42c --- /dev/null +++ b/internal/view/cmd/types.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd + +import "regexp" + +const ( + cowCmd = "cow" + canCmd = "can" + nsFlag = "-n" + filterFlag = "/" + labelFlag = "=" + fuzzyFlag = "-f" + contextFlag = "@" +) + +var ( + rbacRX = regexp.MustCompile(`^can\s+([u|g|s]):\s*([\w-:]+)\s*$`) + + contextCmd = map[string]struct{}{ + "ctx": {}, + "context": {}, + "contexts": {}, + } + 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": {}, + } + xrayCmd = map[string]struct{}{ + "x": {}, + "xr": {}, + "xray": {}, + } +) diff --git a/internal/view/command.go b/internal/view/command.go index 2baae3ac80..0fdb7f7a8f 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -14,19 +14,18 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/view/cmd" "github.com/rs/zerolog/log" ) var ( customViewers MetaViewers - - canRX = regexp.MustCompile(`\Acan\s([u|g|s]):([\w-:]+)\b`) + contextRX = regexp.MustCompile(`\s+@([\w-]+)`) ) // Command represents a user command. type Command struct { - app *App - + app *App alias *dao.Alias mx sync.Mutex } @@ -39,9 +38,9 @@ func NewCommand(app *App) *Command { } // Init initializes the command. -func (c *Command) Init() error { +func (c *Command) Init(path string) error { c.alias = dao.NewAlias(c.app.factory) - if _, err := c.alias.Ensure(); err != nil { + if _, err := c.alias.Ensure(path); err != nil { log.Error().Err(err).Msgf("command init failed!") return err } @@ -51,14 +50,14 @@ func (c *Command) Init() error { } // Reset resets Command and reload aliases. -func (c *Command) Reset(clear bool) error { +func (c *Command) Reset(path string, clear bool) error { c.mx.Lock() defer c.mx.Unlock() if clear { c.alias.Clear() } - if _, err := c.alias.Ensure(); err != nil { + if _, err := c.alias.Ensure(path); err != nil { return err } @@ -74,172 +73,203 @@ func allowedXRay(gvr client.GVR) bool { "apps/v1/statefulsets": {}, "apps/v1/replicasets": {}, } - _, ok := gg[gvr.String()] + return ok } -func (c *Command) xrayCmd(cmd string) error { - tokens := strings.Split(cmd, " ") - if len(tokens) < 2 { - return errors.New("you must specify a resource") +func (c *Command) contextCmd(p *cmd.Interpreter) error { + ctx, ok := p.ContextArg() + if !ok { + return fmt.Errorf("invalid command use `context xxx`") + } + + if ctx != "" { + return useContext(c.app, ctx) + } + + gvr, v, err := c.viewMetaFor(p) + if err != nil { + return err + } + + return c.exec(p, gvr, c.componentFor(gvr, ctx, v), true) +} + +func (c *Command) xrayCmd(p *cmd.Interpreter) error { + arg, cns, ok := p.XrayArgs() + if !ok { + return errors.New("invalid command. use `xray xxx`") } - gvr, ok := c.alias.AsGVR(tokens[1]) + gvr, _, ok := c.alias.AsGVR(arg) if !ok { - return fmt.Errorf("`%s` command not found", cmd) + return fmt.Errorf("invalid resource name: %q", arg) } if !allowedXRay(gvr) { - return fmt.Errorf("`%s` command not found", cmd) + return fmt.Errorf("unsupported resource %q", arg) } - - x := NewXray(gvr) ns := c.app.Config.ActiveNamespace() - if len(tokens) == 3 { - ns = tokens[2] + if cns != "" { + ns = cns } if err := c.app.Config.SetActiveNamespace(client.CleanseNamespace(ns)); err != nil { return err } + if err := c.app.switchNS(ns); err != nil { + return err + } + if err := c.app.Config.Save(); err != nil { return err } - return c.exec(cmd, "xrays", x, true) + return c.exec(p, client.NewGVR("xrays"), NewXray(gvr), true) } // Run execs the command by showing associated display. -func (c *Command) run(cmd, path string, clearStack bool) error { - if c.specialCmd(cmd, path) { +func (c *Command) run(p *cmd.Interpreter, fqn string, clearStack bool) error { + if c.specialCmd(p) { return nil } - cmds := strings.Split(cmd, " ") - command := strings.ToLower(cmds[0]) - gvr, v, err := c.viewMetaFor(command) + // if _, ok := c.alias.Check(p.Cmd()); !ok { + // return fmt.Errorf("command not found %q", p.Cmd()) + // } + + gvr, v, err := c.viewMetaFor(p) if err != nil { return err } - var cns string - tt := strings.Split(gvr, " ") - if len(tt) == 2 { - gvr, cns = tt[0], tt[1] + + ns := c.app.Config.ActiveNamespace() + if cns, ok := p.NSArg(); ok { + ns = cns + } + if err := c.app.switchNS(ns); err != nil { + return err } - switch command { - case "ctx", "context", "contexts": - if len(cmds) == 2 { - return useContext(c.app, cmds[1]) - } - return c.exec(cmd, gvr, c.componentFor(gvr, path, v), clearStack) - case "dir": - if len(cmds) != 2 { - return errors.New("you must specify a directory") - } - return c.app.dirCmd(cmds[1]) - default: - // checks if Command includes a namespace - ns := c.app.Config.ActiveNamespace() - if len(cmds) == 2 { - ns = cmds[1] + if context, ok := p.HasContext(); ok { + res, err := dao.AccessorFor(c.app.factory, client.NewGVR("contexts")) + if err != nil { + return err } - if cns != "" { - ns = cns + switcher, ok := res.(dao.Switchable) + if !ok { + return errors.New("expecting a switchable resource") } - if err := c.app.switchNS(ns); err != nil { + if err := switcher.Switch(context); err != nil { + log.Error().Err(err).Msgf("Context switch failed") return err } - if !c.alias.Check(command) { - return fmt.Errorf("`%s` Command not found", cmd) + + if err := c.app.switchContext(p); err != nil { + return err } - return c.exec(cmd, gvr, c.componentFor(gvr, path, v), clearStack) } + + co := c.componentFor(gvr, fqn, v) + co.SetFilter("") + co.SetLabelFilter(nil) + if f, ok := p.FilterArg(); ok { + co.SetFilter(f) + } + if ll, ok := p.LabelsArg(); ok { + co.SetLabelFilter(ll) + } + + return c.exec(p, gvr, co, clearStack) } func (c *Command) defaultCmd() error { if c.app.Conn() == nil || !c.app.Conn().ConnectionOK() { - return c.run("context", "", true) + return c.run(cmd.NewInterpreter("context"), "", true) } - view := c.app.Config.ActiveView() - if view == "" { - return c.run("pod", "", true) - } - tokens := strings.Split(view, " ") - cmd := view - if len(tokens) == 1 { - if !isContextCmd(tokens[0]) { - cmd = tokens[0] + " " + c.app.Config.ActiveNamespace() - } + + p := cmd.NewInterpreter(c.app.Config.ActiveView()) + if p.IsBlank() { + return c.run(p.Reset("pod"), "", true) } - if err := c.run(cmd, "", true); err != nil { - log.Error().Err(err).Msgf("Default run command failed %q", cmd) - return c.run("pod", "", true) + if err := c.run(p, "", true); err != nil { + log.Error().Err(err).Msgf("Default run command failed %q", p.GetLine()) + return c.run(p.Reset("pod"), "", true) } - return nil -} -func isContextCmd(c string) bool { - return c == "ctx" || c == "context" + return nil } -func (c *Command) specialCmd(cmd, path string) bool { - cmds := strings.Split(cmd, " ") - switch cmds[0] { - case "cow": - c.app.cowCmd(path) - return true - case "q", "q!", "qa", "Q", "quit": +func (c *Command) specialCmd(p *cmd.Interpreter) bool { + switch { + case p.IsCowCmd(): + if msg, ok := p.CowArg(); !ok { + c.app.Flash().Errf("Invalid command. Use `cow xxx`") + } else { + c.app.cowCmd(msg) + } + case p.IsBailCmd(): c.app.BailOut() - return true - case "?", "h", "help": - c.app.helpCmd(nil) - return true - case "a", "alias": - c.app.aliasCmd(nil) - return true - case "x", "xray": - if err := c.xrayCmd(cmd); err != nil { + case p.IsHelpCmd(): + _ = c.app.helpCmd(nil) + case p.IsAliasCmd(): + _ = c.app.aliasCmd(nil) + case p.IsXrayCmd(): + if err := c.xrayCmd(p); err != nil { c.app.Flash().Err(err) } - return true - default: - if !canRX.MatchString(cmd) { - return false + case p.IsRBACCmd(): + if cat, sub, ok := p.RBACArgs(); !ok { + c.app.Flash().Errf("Invalid command. Use `can [u|g|s]:xxx`") + } else if err := c.app.inject(NewPolicy(c.app, cat, sub), true); err != nil { + c.app.Flash().Err(err) } - tokens := canRX.FindAllStringSubmatch(cmd, -1) - if len(tokens) == 1 && len(tokens[0]) == 3 { - if err := c.app.inject(NewPolicy(c.app, tokens[0][1], tokens[0][2]), false); err != nil { - log.Error().Err(err).Msgf("policy view load failed") - return false - } - return true + case p.IsContextCmd(): + if err := c.contextCmd(p); err != nil { + c.app.Flash().Err(err) } + case p.IsDirCmd(): + if a, ok := p.DirArg(); !ok { + c.app.Flash().Errf("Invalid command. Use `dir xxx`") + } else if err := c.app.dirCmd(a); err != nil { + c.app.Flash().Err(err) + } + default: + return false } - return false + + return true } -func (c *Command) viewMetaFor(cmd string) (string, *MetaViewer, error) { - gvr, ok := c.alias.AsGVR(cmd) +func (c *Command) viewMetaFor(p *cmd.Interpreter) (client.GVR, *MetaViewer, error) { + agvr, exp, ok := c.alias.AsGVR(p.Cmd()) if !ok { - return "", nil, fmt.Errorf("`%s` command not found", cmd) + return client.NoGVR, nil, fmt.Errorf("`%s` command not found", p.Cmd()) + } + gvr := agvr + if exp != "" { + ff := strings.Fields(exp) + ff[0] = agvr.String() + ap := cmd.NewInterpreter(strings.Join(ff, " ")) + gvr = client.NewGVR(ap.Cmd()) + p.Amend(ap) } - v, ok := customViewers[gvr] - if !ok { - return gvr.String(), &MetaViewer{viewerFn: NewBrowser}, nil + v := MetaViewer{viewerFn: NewBrowser} + if mv, ok := customViewers[gvr]; ok { + v = mv } - return gvr.String(), &v, nil + return gvr, &v, nil } -func (c *Command) componentFor(gvr, path string, v *MetaViewer) ResourceViewer { +func (c *Command) componentFor(gvr client.GVR, fqn string, v *MetaViewer) ResourceViewer { var view ResourceViewer if v.viewerFn != nil { - view = v.viewerFn(client.NewGVR(gvr)) + view = v.viewerFn(gvr) } else { - view = NewBrowser(client.NewGVR(gvr)) + view = NewBrowser(gvr) } - view.SetInstance(path) + view.SetInstance(fqn) if v.enterFn != nil { view.GetTable().SetEnterFn(v.enterFn) } @@ -247,7 +277,7 @@ func (c *Command) componentFor(gvr, path string, v *MetaViewer) ResourceViewer { return view } -func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) (err error) { +func (c *Command) exec(p *cmd.Interpreter, gvr client.GVR, comp model.Component, clearStack bool) (err error) { defer func() { if e := recover(); e != nil { log.Error().Msgf("Something bad happened! %#v", e) @@ -255,33 +285,30 @@ func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) ( log.Debug().Msgf("History %v", c.app.cmdHistory.List()) log.Error().Msg(string(debug.Stack())) - hh := c.app.cmdHistory.List() - if len(hh) == 0 { - _ = c.run("pod", "", true) - } else { - _ = c.run(hh[0], "", true) + p := cmd.NewInterpreter("pod") + if cmd := c.app.cmdHistory.Pop(); cmd != "" { + p = p.Reset(cmd) } - err = fmt.Errorf("invalid command %q", cmd) + err = c.run(p, "", true) } }() if comp == nil { return fmt.Errorf("no component found for %s", gvr) } - c.app.Flash().Infof("Viewing %s...", client.NewGVR(gvr).R()) - command := cmd - if tokens := strings.Split(cmd, " "); len(tokens) >= 2 { - command = tokens[0] - } - c.app.Config.SetActiveView(command) - if err := c.app.Config.Save(); err != nil { - log.Error().Err(err).Msg("Config save failed!") + c.app.Flash().Infof("Viewing %s...", gvr.R()) + if clearStack { + cmd := contextRX.ReplaceAllString(p.GetLine(), "") + c.app.Config.SetActiveView(cmd) + if err := c.app.Config.Save(); err != nil { + log.Error().Err(err).Msg("Config save failed!") + } } if err := c.app.inject(comp, clearStack); err != nil { return err } - c.app.cmdHistory.Push(cmd) + c.app.cmdHistory.Push(p.GetLine()) return } diff --git a/internal/view/container.go b/internal/view/container.go index 6094fd2ed0..7a2238a3fa 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -110,7 +110,7 @@ func (c *Container) logOptions(prev bool) (*dao.LogOptions, error) { return &opts, nil } -func (c *Container) viewLogs(app *App, model ui.Tabular, gvr, path string) { +func (c *Container) viewLogs(app *App, model ui.Tabular, gvr client.GVR, path string) { c.ResourceViewer.(*LogsExtender).showLogs(c.GetTable().Path, false) } @@ -136,7 +136,10 @@ func (c *Container) showPFCmd(evt *tcell.EventKey) *tcell.EventKey { } func (c *Container) portForwardContext(ctx context.Context) context.Context { - ctx = context.WithValue(ctx, internal.KeyBenchCfg, c.App().BenchFile) + if bc := c.App().BenchFile; bc != "" { + ctx = context.WithValue(ctx, internal.KeyBenchCfg, c.App().BenchFile) + } + return context.WithValue(ctx, internal.KeyPath, c.GetTable().Path) } diff --git a/internal/view/context.go b/internal/view/context.go index 97a5c91b5d..66d4c45e36 100644 --- a/internal/view/context.go +++ b/internal/view/context.go @@ -10,6 +10,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/view/cmd" "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/rs/zerolog/log" @@ -101,7 +102,7 @@ func (c *Context) makeStyledForm() *tview.Form { return f } -func (c *Context) useCtx(app *App, model ui.Tabular, gvr, path string) { +func (c *Context) useCtx(app *App, model ui.Tabular, gvr client.GVR, path string) { log.Debug().Msgf("SWITCH CTX %q--%q", gvr, path) if err := useContext(app, path); err != nil { app.Flash().Err(err) @@ -128,5 +129,5 @@ func useContext(app *App, name string) error { return err } - return app.switchContext(name) + return app.switchContext(cmd.NewInterpreter("ctx " + name)) } diff --git a/internal/view/cronjob.go b/internal/view/cronjob.go index 940904111a..90943a812d 100644 --- a/internal/view/cronjob.go +++ b/internal/view/cronjob.go @@ -42,9 +42,9 @@ func NewCronJob(gvr client.GVR) ResourceViewer { return &c } -func (c *CronJob) showJobs(app *App, model ui.Tabular, gvr, path string) { +func (c *CronJob) showJobs(app *App, model ui.Tabular, gvr client.GVR, path string) { log.Debug().Msgf("Showing Jobs %q:%q -- %q", model.GetNamespace(), gvr, path) - o, err := app.factory.Get(gvr, path, true, labels.Everything()) + o, err := app.factory.Get(gvr.String(), path, true, labels.Everything()) if err != nil { app.Flash().Err(err) return diff --git a/internal/view/details.go b/internal/view/details.go index 86ca3a700c..04cbf6577b 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -58,6 +58,9 @@ func NewDetails(app *App, title, subject, contentType string, searchable bool) * return &d } +func (d *Details) SetFilter(string) {} +func (d *Details) SetLabelFilter(map[string]string) {} + // Init initializes the viewer. func (d *Details) Init(_ context.Context) error { if d.title != "" { @@ -294,7 +297,7 @@ func (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveYAML(d.app.Config.K9s.GetScreenDumpDir(), d.app.Config.K9s.CurrentContextDir(), d.title, d.text.GetText(true)); err != nil { + if path, err := saveYAML(d.app.Config.K9s.GetScreenDumpDir(), d.app.Config.K9s.ActiveContextDir(), d.title, d.text.GetText(true)); err != nil { d.app.Flash().Err(err) } else { d.app.Flash().Infof("Log %s saved successfully!", path) diff --git a/internal/view/dir.go b/internal/view/dir.go index 5d30276cd1..a55220a0eb 100644 --- a/internal/view/dir.go +++ b/internal/view/dir.go @@ -12,7 +12,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" "github.com/derailed/tcell/v2" @@ -163,7 +163,7 @@ func isKustomized(sel string) bool { } kk := []string{kustomizeNoExt, kustomizeYAML, kustomizeYML} for _, f := range ff { - if config.InList(kk, f.Name()) { + if data.InList(kk, f.Name()) { return true } } diff --git a/internal/view/dp.go b/internal/view/dp.go index 9af1bd93bb..11decf20cb 100644 --- a/internal/view/dp.go +++ b/internal/view/dp.go @@ -91,7 +91,7 @@ func (d *Deploy) logOptions(prev bool) (*dao.LogOptions, error) { return &opts, nil } -func (d *Deploy) showPods(app *App, model ui.Tabular, gvr, fqn string) { +func (d *Deploy) showPods(app *App, model ui.Tabular, gvr client.GVR, fqn string) { var ddp dao.Deployment ddp.Init(d.App().factory, d.GVR()) diff --git a/internal/view/ds.go b/internal/view/ds.go index ab92227364..4bb9dd6c1b 100644 --- a/internal/view/ds.go +++ b/internal/view/ds.go @@ -43,7 +43,7 @@ func (d *DaemonSet) bindKeys(aa ui.KeyActions) { }) } -func (d *DaemonSet) showPods(app *App, model ui.Tabular, _, path string) { +func (d *DaemonSet) showPods(app *App, model ui.Tabular, _ client.GVR, path string) { var res dao.DaemonSet res.Init(app.factory, d.GVR()) diff --git a/internal/view/exec.go b/internal/view/exec.go index 7bdcef004d..f4f45bf4e7 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -64,7 +64,7 @@ func runK(a *App, opts shellOpts) error { if isInsecure := a.Conn().Config().Flags().Insecure; isInsecure != nil && *isInsecure { args = append(args, "--insecure-skip-tls-verify") } - args = append(args, "--context", a.Config.K9s.CurrentContext) + args = append(args, "--context", a.Config.K9s.ActiveContextName()) if cfg := a.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { args = append(args, "--kubeconfig", *cfg) } @@ -198,7 +198,7 @@ func runKu(a *App, opts shellOpts) (string, error) { if g, err := a.Conn().Config().ImpersonateGroups(); err == nil { args = append(args, "--as-group", g) } - args = append(args, "--context", a.Config.K9s.CurrentContext) + args = append(args, "--context", a.Config.K9s.ActiveContextName()) if cfg := a.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { args = append(args, "--kubeconfig", *cfg) } @@ -284,8 +284,11 @@ func sshIn(a *App, fqn, co string) error { } func nukeK9sShell(a *App) error { - clName := a.Config.K9s.CurrentCluster - if !a.Config.K9s.Clusters[clName].FeatureGates.NodeShell { + ct, err := a.Config.K9s.ActiveContext() + if err != nil { + return err + } + if !ct.FeatureGates.NodeShell { return nil } diff --git a/internal/view/helm_chart.go b/internal/view/helm_chart.go index d4624e7a3d..afa58e5056 100644 --- a/internal/view/helm_chart.go +++ b/internal/view/helm_chart.go @@ -45,7 +45,7 @@ func (c *HelmChart) bindKeys(aa ui.KeyActions) { }) } -func (c *HelmChart) viewReleases(app *App, model ui.Tabular, _, path string) { +func (c *HelmChart) viewReleases(app *App, model ui.Tabular, _ client.GVR, path string) { v := NewHistory(client.NewGVR("helm-history")) v.SetContextFn(c.helmContext) if err := app.inject(v, false); err != nil { @@ -58,7 +58,7 @@ func (c *HelmChart) historyCmd(evt *tcell.EventKey) *tcell.EventKey { if path == "" { return evt } - c.viewReleases(c.App(), c.GetTable().GetModel(), c.GVR().String(), path) + c.viewReleases(c.App(), c.GetTable().GetModel(), c.GVR(), path) return nil } diff --git a/internal/view/helm_history.go b/internal/view/helm_history.go index 185acaeb4d..ce746ff682 100644 --- a/internal/view/helm_history.go +++ b/internal/view/helm_history.go @@ -10,6 +10,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render/helm" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" @@ -19,6 +20,8 @@ import ( // History represents a helm History view. type History struct { ResourceViewer + + Values *model.RevValues } // NewHistory returns a new helm-history view. @@ -31,6 +34,7 @@ func NewHistory(gvr client.GVR) ResourceViewer { h.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone)) h.AddBindKeysFn(h.bindKeys) h.SetContextFn(h.HistoryContext) + h.GetTable().SetEnterFn(h.getValsCmd) return &h } @@ -62,6 +66,21 @@ func (h *History) bindKeys(aa ui.KeyActions) { }) } +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 { + app.Flash().Err(fmt.Errorf("unable to parse version in %q", path)) + return + } + name, rev := tt[0], tt[1] + h.Values = model.NewRevValues(h.GVR(), client.FQN(ns, name), rev) + v := NewLiveView(h.App(), "Values", h.Values) + if err := v.app.inject(v, false); err != nil { + v.app.Flash().Err(err) + } +} + func (h *History) bindDangerousKeys(aa ui.KeyActions) { aa.Add(ui.KeyActions{ ui.KeyR: ui.NewKeyAction("RollBackTo...", h.rollbackCmd, true), diff --git a/internal/view/help.go b/internal/view/help.go index d5ff15cb4d..fd9da2ab13 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -43,6 +43,9 @@ func NewHelp(app *App) *Help { } } +func (h *Help) SetFilter(string) {} +func (h *Help) SetLabelFilter(map[string]string) {} + // Init initializes the component. func (h *Help) Init(ctx context.Context) error { if err := h.Table.Init(ctx); err != nil { diff --git a/internal/view/helpers.go b/internal/view/helpers.go index 6805e40519..5f7f4365d8 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -99,27 +99,28 @@ func defaultEnv(c *client.Config, path string, header render.Header, row render. return env } -func describeResource(app *App, m ui.Tabular, gvr, path string) { - v := NewLiveView(app, "Describe", model.NewDescribe(client.NewGVR(gvr), path)) +func describeResource(app *App, m 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) } } -func showPodsWithLabels(app *App, path string, sel map[string]string) { - labels := make([]string, 0, len(sel)) - for k, v := range sel { - labels = append(labels, fmt.Sprintf("%s=%s", k, v)) +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)) } - showPods(app, path, strings.Join(labels, ","), "") + + return strings.Join(ll, ",") } func showPods(app *App, path, labelSel, fieldSel string) { - if err := app.switchNS(client.AllNamespaces); err != nil { - app.Flash().Err(err) - return - } - + // !!BOZO!! needed?? + // if err := app.switchNS(client.BlankNamespace); err != nil { + // app.Flash().Err(err) + // return + // } v := NewPod(client.NewGVR("v1/pods")) v.SetContextFn(podCtx(app, path, labelSel, fieldSel)) @@ -137,14 +138,6 @@ func podCtx(app *App, path, labelSel, fieldSel string) ContextFunc { ctx = context.WithValue(ctx, internal.KeyPath, path) ctx = context.WithValue(ctx, internal.KeyLabels, labelSel) - ns, _ := client.Namespaced(path) - mx := client.NewMetricsServer(app.factory.Client()) - nmx, err := mx.FetchPodsMetrics(ctx, ns) - if err != nil { - log.Debug().Err(err).Msgf("No pods metrics") - } - ctx = context.WithValue(ctx, internal.KeyMetrics, nmx) - return context.WithValue(ctx, internal.KeyFields, fieldSel) } } @@ -254,8 +247,12 @@ func linesWithRegions(lines []string, matches fuzzy.Matches) []string { for i, m := range matches { for _, loc := range dao.ContinuousRanges(m.MatchedIndexes) { start, end := loc[0]+offsetForLine[m.Index], loc[1]+offsetForLine[m.Index] - regionStr := matchTag(i, ll[m.Index][start:end]) - ll[m.Index] = ll[m.Index][:start] + regionStr + ll[m.Index][end:] + line := ll[m.Index] + if end > len(line) { + end = len(line) + } + regionStr := matchTag(i, line[start:end]) + ll[m.Index] = line[:start] + regionStr + line[end:] offsetForLine[m.Index] += len(regionStr) - (end - start) } } diff --git a/internal/view/helpers_test.go b/internal/view/helpers_test.go index d1d144f054..0278afaa02 100644 --- a/internal/view/helpers_test.go +++ b/internal/view/helpers_test.go @@ -11,6 +11,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/render" "github.com/derailed/tcell/v2" "github.com/rs/zerolog" @@ -59,7 +60,7 @@ func TestParsePFAnn(t *testing.T) { } func TestExtractApp(t *testing.T) { - app := NewApp(config.NewConfig(nil)) + app := NewApp(mock.NewMockConfig()) uu := map[string]struct { app *App diff --git a/internal/view/img_scan.go b/internal/view/img_scan.go index 811e1af0e8..edc6501129 100644 --- a/internal/view/img_scan.go +++ b/internal/view/img_scan.go @@ -51,7 +51,7 @@ func (c *ImageScan) bindKeys(aa ui.KeyActions) { }) } -func (s *ImageScan) viewCVE(app *App, model ui.Tabular, gvr, path string) { +func (s *ImageScan) viewCVE(app *App, _ ui.Tabular, _ client.GVR, path string) { bin := browseLinux if runtime.GOOS == "darwin" { bin = browseOSX diff --git a/internal/view/job.go b/internal/view/job.go index 24df7d38d1..09ff8d6854 100644 --- a/internal/view/job.go +++ b/internal/view/job.go @@ -26,8 +26,8 @@ func NewJob(gvr client.GVR) ResourceViewer { return &j } -func (*Job) showPods(app *App, model ui.Tabular, gvr, path string) { - o, err := app.factory.Get(gvr, path, true, labels.Everything()) +func (*Job) showPods(app *App, model ui.Tabular, gvr client.GVR, path string) { + o, err := app.factory.Get(gvr.String(), path, true, labels.Everything()) if err != nil { app.Flash().Err(err) return diff --git a/internal/view/live_view.go b/internal/view/live_view.go index 245a1ec419..be5ec6d393 100644 --- a/internal/view/live_view.go +++ b/internal/view/live_view.go @@ -60,6 +60,9 @@ func NewLiveView(app *App, title string, m model.ResourceViewer) *LiveView { return &v } +func (v *LiveView) SetFilter(string) {} +func (v *LiveView) SetLabelFilter(map[string]string) {} + // Init initializes the viewer. func (v *LiveView) Init(_ context.Context) error { if v.title != "" { @@ -354,7 +357,7 @@ func (v *LiveView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *LiveView) saveCmd(evt *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.GetScreenDumpDir(), v.app.Config.K9s.CurrentContextDir(), name, sanitizeEsc(v.text.GetText(true))); err != nil { + if _, err := saveYAML(v.app.Config.K9s.GetScreenDumpDir(), v.app.Config.K9s.ActiveContextDir(), name, sanitizeEsc(v.text.GetText(true))); err != nil { v.app.Flash().Err(err) } else { v.app.Flash().Infof("File %q saved successfully!", name) diff --git a/internal/view/live_view_test.go b/internal/view/live_view_test.go index 257e8114bb..923ed3a84d 100644 --- a/internal/view/live_view_test.go +++ b/internal/view/live_view_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/stretchr/testify/assert" ) @@ -18,7 +19,7 @@ apiVersion: v1 the secret name you want to quote to use tls.","title":"secretName","type":"string"}},"required":["http","class","classInSpec"],"type":"object"} ` - v := NewLiveView(NewApp(config.NewConfig(nil)), "fred", nil) + v := NewLiveView(NewApp(mock.NewMockConfig()), "fred", nil) assert.NoError(t, v.Init(context.Background())) v.text.SetText(colorizeYAML(config.Yaml{}, s)) diff --git a/internal/view/log.go b/internal/view/log.go index 2b6732465f..5627a2c2af 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -61,6 +61,9 @@ func NewLog(gvr client.GVR, opts *dao.LogOptions) *Log { return &l } +func (l *Log) SetFilter(string) {} +func (l *Log) SetLabelFilter(map[string]string) {} + // Init initializes the viewer. func (l *Log) Init(ctx context.Context) (err error) { if l.app, err = extractApp(ctx); err != nil { @@ -403,7 +406,7 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey { // SaveCmd dumps the logs to file. func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey { - path, err := saveData(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.CurrentContextDir(), l.model.GetPath(), l.logs.GetText(true)) + path, err := saveData(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.ActiveContextDir(), l.model.GetPath(), l.logs.GetText(true)) if err != nil { l.app.Flash().Err(err) return nil diff --git a/internal/view/log_indicator_test.go b/internal/view/log_indicator_test.go index 0c793f594d..2316354d97 100644 --- a/internal/view/log_indicator_test.go +++ b/internal/view/log_indicator_test.go @@ -18,10 +18,10 @@ func TestLogIndicatorRefresh(t *testing.T) { e string }{ "all-containers": { - view.NewLogIndicator(config.NewConfig(nil), defaults, true), "[::b]AllContainers:[gray::d]Off[-::] [::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n", + view.NewLogIndicator(config.NewConfig(nil), defaults, true), "[::b]AllContainers:[steelblue::d]Off[-::] [::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[steelblue::d]Off[-::] [::b]Timestamps:[steelblue::d]Off[-::] [::b]Wrap:[steelblue::d]Off[-::]\n", }, "plain": { - view.NewLogIndicator(config.NewConfig(nil), defaults, false), "[::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[gray::d]Off[-::] [::b]Timestamps:[gray::d]Off[-::] [::b]Wrap:[gray::d]Off[-::]\n", + view.NewLogIndicator(config.NewConfig(nil), defaults, false), "[::b]Autoscroll:[limegreen::b]On[-::] [::b]FullScreen:[steelblue::d]Off[-::] [::b]Timestamps:[steelblue::d]Off[-::] [::b]Wrap:[steelblue::d]Off[-::]\n", }, } diff --git a/internal/view/log_test.go b/internal/view/log_test.go index f5ae47a35a..9bde19d19e 100644 --- a/internal/view/log_test.go +++ b/internal/view/log_test.go @@ -12,6 +12,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/view" @@ -109,10 +110,15 @@ func TestLogViewSave(t *testing.T) { ii.Lines(0, false, ll) v.Flush(ll) - dir := filepath.Join(app.Config.K9s.GetScreenDumpDir(), app.Config.K9s.CurrentCluster) - c1, _ := os.ReadDir(dir) + dd := "/tmp/test-dumps/na" + assert.NoError(t, ensureDumpDir(dd)) + app.Config.K9s.ScreenDumpDir = "/tmp/test-dumps" + dir := filepath.Join(app.Config.K9s.GetScreenDumpDir(), app.Config.K9s.ActiveContextDir()) + c1, err := os.ReadDir(dir) + assert.NoError(t, err, fmt.Sprintf("Dir: %q", dir)) v.SaveCmd(nil) - c2, _ := os.ReadDir(dir) + c2, err := os.ReadDir(dir) + assert.NoError(t, err, fmt.Sprintf("Dir: %q", dir)) assert.Equal(t, len(c2), len(c1)+1) } @@ -144,5 +150,16 @@ func TestAllContainerKeyBinding(t *testing.T) { // Helpers... func makeApp() *view.App { - return view.NewApp(config.NewConfig(ks{})) + return view.NewApp(mock.NewMockConfig()) +} + +func ensureDumpDir(n string) error { + config.AppDumpsDir = n + if _, err := os.Stat(n); os.IsNotExist(err) { + return os.MkdirAll(n, 0700) + } + if err := os.RemoveAll(n); err != nil { + return err + } + return os.MkdirAll(n, 0700) } diff --git a/internal/view/logger.go b/internal/view/logger.go index a3da224bd7..19c079bfe3 100644 --- a/internal/view/logger.go +++ b/internal/view/logger.go @@ -154,7 +154,7 @@ func (l *Logger) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } func (l *Logger) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveYAML(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.CurrentContextDir(), l.title, l.GetText(true)); err != nil { + if path, err := saveYAML(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.ActiveContextDir(), l.title, l.GetText(true)); err != nil { l.app.Flash().Err(err) } else { l.app.Flash().Infof("Log %s saved successfully!", path) diff --git a/internal/view/node.go b/internal/view/node.go index c1f2274f9c..6e3b17cbe4 100644 --- a/internal/view/node.go +++ b/internal/view/node.go @@ -45,8 +45,12 @@ func (n *Node) bindDangerousKeys(aa ui.KeyActions) { ui.KeyU: ui.NewKeyAction("Uncordon", n.toggleCordonCmd(false), true), ui.KeyR: ui.NewKeyAction("Drain", n.drainCmd, true), }) - cl := n.App().Config.K9s.CurrentCluster - if n.App().Config.K9s.Clusters[cl].FeatureGates.NodeShell { + ct, err := n.App().Config.K9s.ActiveContext() + if err != nil { + log.Error().Err(err).Msgf("No active context located") + return + } + if ct.FeatureGates.NodeShell { aa.Add(ui.KeyActions{ ui.KeyS: ui.NewKeyAction("Shell", n.sshCmd, true), }) @@ -66,8 +70,8 @@ func (n *Node) bindKeys(aa ui.KeyActions) { }) } -func (n *Node) showPods(a *App, _ ui.Tabular, _, path string) { - showPods(a, n.GetTable().GetSelectedItem(), client.AllNamespaces, "spec.nodeName="+path) +func (n *Node) showPods(a *App, _ ui.Tabular, _ client.GVR, path string) { + showPods(a, n.GetTable().GetSelectedItem(), client.BlankNamespace, "spec.nodeName="+path) } func (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/ns.go b/internal/view/ns.go index 263018d5ad..ada763e93a 100644 --- a/internal/view/ns.go +++ b/internal/view/ns.go @@ -5,7 +5,7 @@ package view import ( "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" @@ -41,7 +41,7 @@ func (n *Namespace) bindKeys(aa ui.KeyActions) { }) } -func (n *Namespace) switchNs(app *App, model ui.Tabular, gvr, path string) { +func (n *Namespace) switchNs(app *App, _ ui.Tabular, _ client.GVR, path string) { n.useNamespace(path) app.gotoResource("pods", "", false) } @@ -73,14 +73,14 @@ func (n *Namespace) useNamespace(fqn string) { } } -func (n *Namespace) decorate(data *render.TableData) { - if n.App().Conn() == nil || len(data.RowEvents) == 0 { +func (n *Namespace) decorate(td *render.TableData) { + if n.App().Conn() == nil || len(td.RowEvents) == 0 { return } // checks if all ns is in the list if not add it. - if _, ok := data.RowEvents.FindIndex(client.NamespaceAll); !ok { - data.RowEvents = append(data.RowEvents, + if _, ok := td.RowEvents.FindIndex(client.NamespaceAll); !ok { + td.RowEvents = append(td.RowEvents, render.RowEvent{ Kind: render.EventUnchanged, Row: render.Row{ @@ -91,8 +91,8 @@ func (n *Namespace) decorate(data *render.TableData) { ) } - for _, re := range data.RowEvents { - if config.InList(n.App().Config.FavNamespaces(), re.Row.ID) { + for _, re := range td.RowEvents { + if data.InList(n.App().Config.FavNamespaces(), re.Row.ID) { re.Row.Fields[0] += favNSIndicator re.Kind = render.EventUnchanged } diff --git a/internal/view/ofaas.go b/internal/view/ofaas.go deleted file mode 100644 index d2bc27f286..0000000000 --- a/internal/view/ofaas.go +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package view - -// BOZO!! revamp with latest... -// import ( -// "strings" - -// "github.com/derailed/k9s/internal/client" -// "github.com/derailed/k9s/internal/render" -// "github.com/derailed/k9s/internal/ui" -// ) - -// // OpenFaas represents an OpenFaaS viewer. -// type OpenFaas struct { -// ResourceViewer -// } - -// // NewOpenFaas returns a new viewer. -// func NewOpenFaas(gvr client.GVR) ResourceViewer { -// o := OpenFaas{ResourceViewer: NewBrowser(gvr)} -// o.AddBindKeysFn(o.bindKeys) -// o.GetTable().SetEnterFn(o.showPods) - -// return &o -// } - -// func (o *OpenFaas) bindKeys(aa ui.KeyActions) { -// aa.Add(ui.KeyActions{ -// ui.KeyShiftS: ui.NewKeyAction("Sort Status", o.GetTable().SortColCmd(statusCol, true), false), -// ui.KeyShiftI: ui.NewKeyAction("Sort Invocations", o.GetTable().SortColCmd("INVOCATIONS", false), false), -// ui.KeyShiftR: ui.NewKeyAction("Sort Replicas", o.GetTable().SortColCmd("REPLICAS", false), false), -// ui.KeyShiftL: ui.NewKeyAction("Sort Available", o.GetTable().SortColCmd(availCol, false), false), -// }) -// } - -// func (o *OpenFaas) showPods(a *App, _ ui.Tabular, _, path string) { -// labels := o.GetTable().GetSelectedCell(o.GetTable().NameColIndex() + 3) -// sels := make(map[string]string) - -// tokens := strings.Split(labels, ",") -// for _, t := range tokens { -// s := strings.Split(t, "=") -// sels[s[0]] = s[1] -// } - -// showPodsWithLabels(a, path, sels) -// } diff --git a/internal/view/pf.go b/internal/view/pf.go index bef01c0683..40ded9d65a 100644 --- a/internal/view/pf.go +++ b/internal/view/pf.go @@ -44,13 +44,17 @@ func NewPortForward(gvr client.GVR) ResourceViewer { } func (p *PortForward) portForwardContext(ctx context.Context) context.Context { - return context.WithValue(ctx, internal.KeyBenchCfg, p.App().BenchFile) + if bc := p.App().BenchFile; bc != "" { + return context.WithValue(ctx, internal.KeyBenchCfg, p.App().BenchFile) + } + + return ctx } func (p *PortForward) bindKeys(aa ui.KeyActions) { aa.Add(ui.KeyActions{ tcell.KeyEnter: ui.NewKeyAction("View Benchmarks", p.showBenchCmd, true), - tcell.KeyCtrlL: ui.NewKeyAction("Benchmark Run/Stop", p.toggleBenchCmd, true), + ui.KeyB: ui.NewKeyAction("Benchmark Run/Stop", p.toggleBenchCmd, true), tcell.KeyCtrlD: ui.NewKeyAction("Delete", p.deleteCmd, true), ui.KeyShiftP: ui.NewKeyAction("Sort Ports", p.GetTable().SortColCmd("PORTS", true), false), ui.KeyShiftU: ui.NewKeyAction("Sort URL", p.GetTable().SortColCmd("URL", true), false), @@ -108,16 +112,25 @@ func (p *PortForward) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { } p.App().Status(model.FlashWarn, "Benchmark in progress...") - go p.runBenchmark() + go func() { + if err := p.runBenchmark(); err != nil { + log.Error().Err(err).Msgf("Benchmark run failed") + } + }() return nil } -func (p *PortForward) runBenchmark() { +func (p *PortForward) runBenchmark() error { log.Debug().Msg("Bench starting...") - p.bench.Run(p.App().Config.K9s.CurrentCluster, func() { - log.Debug().Msg("Bench Completed!") + ct, err := p.App().Config.K9s.ActiveContext() + if err != nil { + return err + } + name := p.App().Config.K9s.ActiveContextName() + p.bench.Run(ct.ClusterName, name, func() { + log.Debug().Msgf("Benchmark %q Completed!", name) p.App().QueueUpdate(func() { if p.bench.Canceled() { p.App().Status(model.FlashInfo, "Benchmark canceled") @@ -132,6 +145,8 @@ func (p *PortForward) runBenchmark() { }() }) }) + + return nil } func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/pf_dialog.go b/internal/view/pf_dialog.go index a80e8d8ad9..8a79167ff0 100644 --- a/internal/view/pf_dialog.go +++ b/internal/view/pf_dialog.go @@ -33,7 +33,12 @@ func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpe SetFieldTextColor(styles.FieldFgColor.Color()). SetFieldBackgroundColor(styles.BgColor.Color()) - address := v.App().Config.CurrentCluster().PortForwardAddress + ct, err := v.App().Config.K9s.ActiveContext() + if err != nil { + log.Error().Err(err).Msgf("No active context detected") + return + } + address := ct.PortForwardAddress pf, err := aa.PreferredPorts(ports) if err != nil { diff --git a/internal/view/pf_extender.go b/internal/view/pf_extender.go index 37040a8a3b..0f5710b74a 100644 --- a/internal/view/pf_extender.go +++ b/internal/view/pf_extender.go @@ -125,7 +125,7 @@ func startFwdCB(v ResourceViewer, path string, pts port.PortTunnels) error { } log.Debug().Msgf(">>> Starting port forward %q -- %#v", pf.ID(), pt) go runForward(v, pf, fwd) - tt = append(tt, pt.ContainerPort) + tt = append(tt, pt.LocalPort) } if len(tt) == 1 { v.App().Flash().Infof("PortForward activated %s", tt[0]) @@ -137,6 +137,10 @@ func startFwdCB(v ResourceViewer, path string, pts port.PortTunnels) error { } func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error { + ct, err := v.App().Config.CurrentContext() + if err != nil { + return err + } mm, anns, err := fetchPodPorts(v.App().factory, path) if err != nil { return err @@ -156,7 +160,7 @@ func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error { return err } - pts, err := pfs.ToTunnels(v.App().Config.CurrentCluster().PortForwardAddress, ports, port.IsPortFree) + pts, err := pfs.ToTunnels(ct.PortForwardAddress, ports, port.IsPortFree) if err != nil { return err } diff --git a/internal/view/picker.go b/internal/view/picker.go index bca0386746..b16042be95 100644 --- a/internal/view/picker.go +++ b/internal/view/picker.go @@ -27,6 +27,9 @@ func NewPicker() *Picker { } } +func (p *Picker) SetFilter(string) {} +func (p *Picker) SetLabelFilter(map[string]string) {} + // Init initializes the view. func (p *Picker) Init(ctx context.Context) error { app, err := extractApp(ctx) diff --git a/internal/view/pod.go b/internal/view/pod.go index c317304ab9..1ef29bab80 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -128,7 +128,7 @@ func (p *Pod) logOptions(prev bool) (*dao.LogOptions, error) { return &opts, nil } -func (p *Pod) showContainers(app *App, model ui.Tabular, gvr, path string) { +func (p *Pod) showContainers(app *App, _ ui.Tabular, _ client.GVR, _ string) { co := NewContainer(client.NewGVR("containers")) co.SetContextFn(p.coContext) if err := app.inject(co, false); err != nil { @@ -186,7 +186,10 @@ func (p *Pod) showPFCmd(evt *tcell.EventKey) *tcell.EventKey { } func (p *Pod) portForwardContext(ctx context.Context) context.Context { - ctx = context.WithValue(ctx, internal.KeyBenchCfg, p.App().BenchFile) + if bc := p.App().BenchFile; bc != "" { + ctx = context.WithValue(ctx, internal.KeyBenchCfg, p.App().BenchFile) + } + return context.WithValue(ctx, internal.KeyPath, p.GetTable().GetSelectedItem()) } @@ -455,7 +458,7 @@ func buildShellArgs(cmd, path, co string, kcfg *string) []string { args := make([]string, 0, 15) args = append(args, cmd, "-it") ns, po := client.Namespaced(path) - if ns != client.AllNamespaces { + if ns != client.BlankNamespace { args = append(args, "-n", ns) } args = append(args, po) diff --git a/internal/view/pod_test.go b/internal/view/pod_test.go index 101bd5da6d..bbbf0378b9 100644 --- a/internal/view/pod_test.go +++ b/internal/view/pod_test.go @@ -9,7 +9,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" ) @@ -25,6 +25,6 @@ func TestPodNew(t *testing.T) { // Helpers... func makeCtx() context.Context { - cfg := config.NewConfig(ks{}) + cfg := mock.NewMockConfig() return context.WithValue(context.Background(), internal.KeyApp, view.NewApp(cfg)) } diff --git a/internal/view/policy.go b/internal/view/policy.go index 35081e99de..28fee83e64 100644 --- a/internal/view/policy.go +++ b/internal/view/policy.go @@ -33,7 +33,7 @@ func NewPolicy(app *App, subject, name string) *Policy { subjectName: name, } p.AddBindKeysFn(p.bindKeys) - p.GetTable().SetSortCol(nameCol, false) + p.GetTable().SetSortCol("API-GROUP", false) p.SetContextFn(p.subjectCtx) p.GetTable().SetEnterFn(blankEnterFn) @@ -50,7 +50,7 @@ func (p *Policy) bindKeys(aa ui.KeyActions) { aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace) aa.Add(ui.KeyActions{ ui.KeyShiftN: ui.NewKeyAction("Sort Name", p.GetTable().SortColCmd(nameCol, true), false), - ui.KeyShiftO: ui.NewKeyAction("Sort Group", p.GetTable().SortColCmd("GROUP", true), false), + ui.KeyShiftA: ui.NewKeyAction("Sort Api-Group", p.GetTable().SortColCmd("API-GROUP", true), false), ui.KeyShiftB: ui.NewKeyAction("Sort Binding", p.GetTable().SortColCmd("BINDING", true), false), }) } diff --git a/internal/view/priorityclass.go b/internal/view/priorityclass.go index 78a0686681..fc89ef5010 100644 --- a/internal/view/priorityclass.go +++ b/internal/view/priorityclass.go @@ -5,6 +5,7 @@ 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" ) @@ -31,5 +32,5 @@ func (s *PriorityClass) bindKeys(aa ui.KeyActions) { } func (s *PriorityClass) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanRefs(evt, s.App(), s.GetTable(), "scheduling.k8s.io/v1/priorityclasses") + return scanRefs(evt, s.App(), s.GetTable(), dao.PcGVR) } diff --git a/internal/view/pulse.go b/internal/view/pulse.go index d8acc0d0c1..c323f64d01 100644 --- a/internal/view/pulse.go +++ b/internal/view/pulse.go @@ -77,6 +77,9 @@ func NewPulse(gvr client.GVR) ResourceViewer { } } +func (p *Pulse) SetFilter(string) {} +func (p *Pulse) SetLabelFilter(map[string]string) {} + // Init initializes the view. func (p *Pulse) Init(ctx context.Context) error { p.SetBorder(true) diff --git a/internal/view/pvc.go b/internal/view/pvc.go index 075ff4efca..486fb46383 100644 --- a/internal/view/pvc.go +++ b/internal/view/pvc.go @@ -5,6 +5,7 @@ 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" ) @@ -35,5 +36,5 @@ func (p *PersistentVolumeClaim) bindKeys(aa ui.KeyActions) { } func (p *PersistentVolumeClaim) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanRefs(evt, p.App(), p.GetTable(), "v1/persistentvolumeclaims") + return scanRefs(evt, p.App(), p.GetTable(), dao.PvcGVR) } diff --git a/internal/view/rbac.go b/internal/view/rbac.go index 0205ee2295..2ea21464e1 100644 --- a/internal/view/rbac.go +++ b/internal/view/rbac.go @@ -23,7 +23,7 @@ func NewRbac(gvr client.GVR) ResourceViewer { ResourceViewer: NewBrowser(gvr), } r.AddBindKeysFn(r.bindKeys) - r.GetTable().SetSortCol("APIGROUP", true) + r.GetTable().SetSortCol("API-GROUP", true) r.GetTable().SetEnterFn(blankEnterFn) return &r @@ -32,11 +32,11 @@ func NewRbac(gvr client.GVR) ResourceViewer { func (r *Rbac) bindKeys(aa ui.KeyActions) { aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace) aa.Add(ui.KeyActions{ - ui.KeyShiftO: ui.NewKeyAction("Sort APIGroup", r.GetTable().SortColCmd("APIGROUP", true), false), + ui.KeyShiftA: ui.NewKeyAction("Sort API-Group", r.GetTable().SortColCmd("API-GROUP", true), false), }) } -func showRules(app *App, _ ui.Tabular, gvr, path string) { +func showRules(app *App, _ ui.Tabular, gvr client.GVR, path string) { v := NewRbac(client.NewGVR("rbac")) v.SetContextFn(rbacCtx(gvr, path)) @@ -45,11 +45,11 @@ func showRules(app *App, _ ui.Tabular, gvr, path string) { } } -func rbacCtx(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, _, _ string) {} +func blankEnterFn(_ *App, _ ui.Tabular, _ client.GVR, _ string) {} diff --git a/internal/view/reference.go b/internal/view/reference.go index d9a76d803a..5519eae907 100644 --- a/internal/view/reference.go +++ b/internal/view/reference.go @@ -33,7 +33,7 @@ func (r *Reference) Init(ctx context.Context) error { if err := r.ResourceViewer.Init(ctx); err != nil { return err } - r.GetTable().GetModel().SetNamespace(client.AllNamespaces) + r.GetTable().GetModel().SetNamespace(client.BlankNamespace) return nil } diff --git a/internal/view/registrar.go b/internal/view/registrar.go index 52532c3633..cde177b9c1 100644 --- a/internal/view/registrar.go +++ b/internal/view/registrar.go @@ -61,6 +61,9 @@ func coreViewers(vv MetaViewers) { } func miscViewers(vv MetaViewers) { + vv[client.NewGVR("workloads")] = MetaViewer{ + viewerFn: NewWorkload, + } vv[client.NewGVR("contexts")] = MetaViewer{ viewerFn: NewContext, } @@ -156,7 +159,7 @@ func extViewers(vv MetaViewers) { } } -func showCRD(app *App, _ ui.Tabular, _, path string) { +func showCRD(app *App, _ ui.Tabular, _ client.GVR, path string) { _, crd := client.Namespaced(path) app.gotoResource(crd, "", false) } diff --git a/internal/view/rs.go b/internal/view/rs.go index 739ce6a9d7..3650b8ff15 100644 --- a/internal/view/rs.go +++ b/internal/view/rs.go @@ -38,7 +38,7 @@ func (r *ReplicaSet) bindKeys(aa ui.KeyActions) { }) } -func (r *ReplicaSet) showPods(app *App, model ui.Tabular, gvr, path string) { +func (r *ReplicaSet) showPods(app *App, _ ui.Tabular, _ client.GVR, path string) { var drs dao.ReplicaSet rs, err := drs.Load(app.factory, path) if err != nil { diff --git a/internal/view/sa.go b/internal/view/sa.go index d4a94a088e..c1433c2cce 100644 --- a/internal/view/sa.go +++ b/internal/view/sa.go @@ -41,7 +41,7 @@ func (s *ServiceAccount) subjectCtx(ctx context.Context) context.Context { } func (s *ServiceAccount) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanSARefs(evt, s.App(), s.GetTable(), "v1/serviceaccounts") + return scanSARefs(evt, s.App(), s.GetTable(), dao.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 string) *tcell.EventKey { +func scanSARefs(evt *tcell.EventKey, a *App, t *Table, gvr client.GVR) *tcell.EventKey { path := t.GetSelectedItem() if path == "" { return evt diff --git a/internal/view/sanitizer.go b/internal/view/sanitizer.go index 2b71d7d46c..bae27e7fa0 100644 --- a/internal/view/sanitizer.go +++ b/internal/view/sanitizer.go @@ -12,6 +12,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/xray" "github.com/derailed/tcell/v2" @@ -24,7 +25,7 @@ import ( var _ ResourceViewer = (*Sanitizer)(nil) -// Sanitizer represents an sanitizer tree view. +// Sanitizer represents a sanitizer tree view. type Sanitizer struct { *ui.Tree @@ -46,6 +47,9 @@ func NewSanitizer(gvr client.GVR) ResourceViewer { } } +func (s *Sanitizer) SetFilter(string) {} +func (s *Sanitizer) SetLabelFilter(map[string]string) {} + // Init initializes the view. func (s *Sanitizer) Init(ctx context.Context) error { s.envFn = s.k9sEnv @@ -96,7 +100,7 @@ func (*Sanitizer) InCmdMode() bool { // ExtraHints returns additional hints. func (s *Sanitizer) ExtraHints() map[string]string { - if s.app.Config.K9s.NoIcons { + if s.app.Config.K9s.UI.NoIcons { return nil } return xray.EmojiInfo() @@ -266,7 +270,7 @@ func (s *Sanitizer) TreeLoadFailed(err error) { } func (s *Sanitizer) update(node *xray.TreeNode) { - root := makeTreeNode(node, s.ExpandNodes(), s.app.Config.K9s.NoIcons, s.app.Styles) + root := makeTreeNode(node, s.ExpandNodes(), s.app.Config.K9s.UI.NoIcons, s.app.Styles) if node == nil { s.app.QueueUpdateDraw(func() { s.SetRoot(root) @@ -313,7 +317,7 @@ func (s *Sanitizer) TreeChanged(node *xray.TreeNode) { } func (s *Sanitizer) hydrate(parent *tview.TreeNode, n *xray.TreeNode) { - node := makeTreeNode(n, s.ExpandNodes(), s.app.Config.K9s.NoIcons, s.app.Styles) + node := makeTreeNode(n, s.ExpandNodes(), s.app.Config.K9s.UI.NoIcons, s.app.Styles) for _, c := range n.Children { s.hydrate(node, c) } @@ -414,9 +418,9 @@ func (s *Sanitizer) styleTitle() string { var title string if ns == client.ClusterScope { - title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, s.Count), s.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, render.AsThousands(int64(s.Count))), s.app.Styles.Frame()) } else { - title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, s.Count), s.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, render.AsThousands(int64(s.Count))), s.app.Styles.Frame()) } buff := s.CmdBuff().GetText() diff --git a/internal/view/screen_dump.go b/internal/view/screen_dump.go index 9f12c30029..9183498c8b 100644 --- a/internal/view/screen_dump.go +++ b/internal/view/screen_dump.go @@ -9,7 +9,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" "github.com/rs/zerolog/log" @@ -28,7 +28,7 @@ func NewScreenDump(gvr client.GVR) ResourceViewer { s.GetTable().SetBorderFocusColor(tcell.ColorSteelBlue) s.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorRoyalBlue).Attributes(tcell.AttrNone)) s.GetTable().SetSortCol(ageCol, true) - s.GetTable().SelectRow(1, true) + s.GetTable().SelectRow(1, 0, true) s.GetTable().SetEnterFn(s.edit) s.SetContextFn(s.dirContext) @@ -36,8 +36,8 @@ func NewScreenDump(gvr client.GVR) ResourceViewer { } func (s *ScreenDump) dirContext(ctx context.Context) context.Context { - dir := filepath.Join(s.App().Config.K9s.GetScreenDumpDir(), s.App().Config.K9s.CurrentContextDir()) - if err := config.EnsureFullPath(dir, config.DefaultDirMod); err != nil { + dir := filepath.Join(s.App().Config.K9s.GetScreenDumpDir(), s.App().Config.K9s.ActiveContextDir()) + if err := data.EnsureFullPath(dir, data.DefaultDirMod); err != nil { s.App().Flash().Err(err) return ctx } @@ -45,7 +45,7 @@ func (s *ScreenDump) dirContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyDir, dir) } -func (s *ScreenDump) edit(app *App, model ui.Tabular, gvr, path string) { +func (s *ScreenDump) edit(app *App, _ ui.Tabular, _ client.GVR, path string) { log.Debug().Msgf("ScreenDump selection is %q", path) s.Stop() diff --git a/internal/view/secret.go b/internal/view/secret.go index 3cc78234c9..3237c24550 100644 --- a/internal/view/secret.go +++ b/internal/view/secret.go @@ -5,6 +5,7 @@ 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" v1 "k8s.io/api/core/v1" @@ -37,7 +38,7 @@ func (s *Secret) bindKeys(aa ui.KeyActions) { } func (s *Secret) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanRefs(evt, s.App(), s.GetTable(), "v1/secrets") + return scanRefs(evt, s.App(), s.GetTable(), dao.SecGVR) } func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/sts.go b/internal/view/sts.go index cbb77a7cd1..e9e04fcae1 100644 --- a/internal/view/sts.go +++ b/internal/view/sts.go @@ -86,7 +86,7 @@ func (s *StatefulSet) bindKeys(aa ui.KeyActions) { }) } -func (s *StatefulSet) showPods(app *App, _ ui.Tabular, _, 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/svc.go b/internal/view/svc.go index b47bd01295..4abf37d772 100644 --- a/internal/view/svc.go +++ b/internal/view/svc.go @@ -48,12 +48,12 @@ func NewService(gvr client.GVR) ResourceViewer { func (s *Service) bindKeys(aa ui.KeyActions) { aa.Add(ui.KeyActions{ - tcell.KeyCtrlL: ui.NewKeyAction("Bench Run/Stop", s.toggleBenchCmd, true), - ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd("TYPE", true), false), + ui.KeyB: ui.NewKeyAction("Bench Run/Stop", s.toggleBenchCmd, true), + ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd("TYPE", true), false), }) } -func (s *Service) showPods(a *App, _ ui.Tabular, 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()) @@ -71,7 +71,7 @@ func (s *Service) showPods(a *App, _ ui.Tabular, gvr, path string) { return } - showPodsWithLabels(a, path, svc.Spec.Selector) + showPods(a, path, toLabelsStr(svc.Spec.Selector), "") } func (s *Service) checkSvc(svc *v1.Service) error { @@ -153,14 +153,30 @@ func (s *Service) runBenchmark(port string, cfg config.BenchConfig) error { } var err error - base := "http://" + cfg.HTTP.Host + ":" + port + cfg.HTTP.Path + base := cfg.HTTP.Host + if !strings.Contains(base, ":") { + base += ":" + port + cfg.HTTP.Path + } else { + base += cfg.HTTP.Path + } + if strings.Index(base, "http") != 0 { + base = "http://" + base + } + if s.bench, err = perf.NewBenchmark(base, s.App().version, cfg); err != nil { return err } s.App().Status(model.FlashWarn, "Benchmark in progress...") - log.Debug().Msg("Bench starting...") - go s.bench.Run(s.App().Config.K9s.CurrentCluster, s.benchDone) + log.Debug().Msg("Benchmark starting...") + + ct, err := s.App().Config.K9s.ActiveContext() + if err != nil { + return err + } + name := s.App().Config.K9s.ActiveContextName() + + go s.bench.Run(ct.ClusterName, name, s.benchDone) return nil } diff --git a/internal/view/table.go b/internal/view/table.go index 4db21ff17f..a2ce9aabf7 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -170,7 +170,7 @@ func (t *Table) BufferActive(state bool, k model.BufferKind) { } func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveTable(t.app.Config.K9s.GetScreenDumpDir(), t.app.Config.K9s.CurrentContextDir(), t.GVR().R(), t.Path, t.GetFilteredData()); err != nil { + if path, err := saveTable(t.app.Config.K9s.GetScreenDumpDir(), t.app.Config.K9s.ActiveContextDir(), t.GVR().R(), t.Path, t.GetFilteredData()); err != nil { t.app.Flash().Err(err) } else { t.app.Flash().Infof("File %s saved successfully!", path) diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go index bb20ce490c..32e21b2dac 100644 --- a/internal/view/table_int_test.go +++ b/internal/view/table_int_test.go @@ -13,13 +13,13 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -29,7 +29,8 @@ func TestTableSave(t *testing.T) { assert.NoError(t, v.Init(makeContext())) v.SetTitle("k9s-test") - dir := filepath.Join(v.app.Config.K9s.GetScreenDumpDir(), v.app.Config.K9s.CurrentCluster) + assert.NoError(t, ensureDumpDir("/tmp/test-dumps")) + dir := filepath.Join(v.app.Config.K9s.GetScreenDumpDir(), v.app.Config.K9s.ActiveContextDir()) c1, _ := os.ReadDir(dir) v.saveCmd(nil) @@ -128,6 +129,7 @@ var _ ui.Tabular = (*mockTableModel)(nil) 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) Count() int { return 1 } func (t *mockTableModel) HasMetrics() bool { return true } @@ -195,29 +197,40 @@ func makeTableData() *render.TableData { } func makeContext() context.Context { - a := NewApp(config.NewConfig(ks{})) + a := NewApp(mock.NewMockConfig()) ctx := context.WithValue(context.Background(), internal.KeyApp, a) return context.WithValue(ctx, internal.KeyStyles, a.Styles) } -type ks struct{} +// type ks struct{} -func (k ks) CurrentContextName() (string, error) { - return "test", nil -} +// func (k ks) CurrentContextName() (string, error) { +// return "test", nil +// } -func (k ks) CurrentClusterName() (string, error) { - return "test", nil -} +// func (k ks) CurrentClusterName() (string, error) { +// return "test", nil +// } -func (k ks) CurrentNamespaceName() (string, error) { - return "test", nil -} +// func (k ks) CurrentNamespaceName() (string, error) { +// return "test", nil +// } -func (k ks) ClusterNames() (map[string]struct{}, error) { - return map[string]struct{}{"test": {}}, nil -} +// func (k ks) ContextNames() (map[string]struct{}, error) { +// return map[string]struct{}{"test": {}}, nil +// } -func (k ks) NamespaceNames(nn []v1.Namespace) []string { - return []string{"test"} +// func (k ks) NamespaceNames(nn []v1.Namespace) []string { +// return []string{"test"} +// } + +func ensureDumpDir(n string) error { + config.AppDumpsDir = n + if _, err := os.Stat(n); os.IsNotExist(err) { + return os.Mkdir(n, 0700) + } + if err := os.RemoveAll(n); err != nil { + return err + } + return os.Mkdir(n, 0700) } diff --git a/internal/view/types.go b/internal/view/types.go index 6a1316f8c2..070db98c79 100644 --- a/internal/view/types.go +++ b/internal/view/types.go @@ -31,7 +31,7 @@ type ( BoostActionsFunc func(ui.KeyActions) // EnterFunc represents an enter key action. - EnterFunc func(app *App, model ui.Tabular, 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) diff --git a/internal/view/value_extender.go b/internal/view/value_extender.go index a7cf7f8c61..795da9f740 100644 --- a/internal/view/value_extender.go +++ b/internal/view/value_extender.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -20,7 +23,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, path string) { + p.GetTable().SetEnterFn(func(app *App, model ui.Tabular, gvr client.GVR, path string) { p.valuesCmd(nil) }) diff --git a/internal/view/vul_extender.go b/internal/view/vul_extender.go index 2bd59b93a3..5c1774d62c 100644 --- a/internal/view/vul_extender.go +++ b/internal/view/vul_extender.go @@ -26,7 +26,7 @@ func NewVulnerabilityExtender(r ResourceViewer) ResourceViewer { } func (v *VulnerabilityExtender) bindKeys(aa ui.KeyActions) { - if v.App().Config.K9s.EnableImageScan { + if v.App().Config.K9s.ImageScans.Enable { aa.Add(ui.KeyActions{ ui.KeyV: ui.NewKeyAction("Show Vulnerabilities", v.showVulCmd, true), ui.KeyShiftV: ui.NewKeyAction("Sort Vulnerabilities", v.GetTable().SortColCmd("VS", true), false), diff --git a/internal/view/workload.go b/internal/view/workload.go new file mode 100644 index 0000000000..dd4d43b66d --- /dev/null +++ b/internal/view/workload.go @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package view + +import ( + "context" + "fmt" + "strings" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/ui/dialog" + "github.com/derailed/tcell/v2" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Workload presents a workload viewer. +type Workload struct { + ResourceViewer +} + +// NewWorkload returns a new viewer. +func NewWorkload(gvr client.GVR) ResourceViewer { + w := Workload{ + ResourceViewer: NewBrowser(gvr), + } + w.GetTable().SetEnterFn(w.showRes) + w.AddBindKeysFn(w.bindKeys) + w.GetTable().SetSortCol("KIND", true) + + return &w +} + +func (w *Workload) bindDangerousKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + ui.KeyE: ui.NewKeyAction("Edit", w.editCmd, true), + tcell.KeyCtrlD: ui.NewKeyAction("Delete", w.deleteCmd, true), + }) +} + +func (w *Workload) bindKeys(aa ui.KeyActions) { + if !w.App().Config.K9s.IsReadOnly() { + w.bindDangerousKeys(aa) + } + + aa.Add(ui.KeyActions{ + ui.KeyShiftK: ui.NewKeyAction("Sort Kind", w.GetTable().SortColCmd("KIND", true), false), + ui.KeyShiftS: ui.NewKeyAction("Sort Status", w.GetTable().SortColCmd(statusCol, true), false), + ui.KeyShiftR: ui.NewKeyAction("Sort Ready", w.GetTable().SortColCmd("READY", true), false), + ui.KeyShiftA: ui.NewKeyAction("Sort Age", w.GetTable().SortColCmd(ageCol, true), false), + ui.KeyY: ui.NewKeyAction(yamlAction, w.yamlCmd, true), + ui.KeyD: ui.NewKeyAction("Describe", w.describeCmd, true), + }) +} + +func parsePath(path string) (client.GVR, string, bool) { + tt := strings.Split(path, "|") + if len(tt) != 3 { + log.Error().Msgf("unable to parse path: %q", path) + return client.NewGVR(""), 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) { + gvr, fqn, ok := parsePath(path) + if !ok { + app.Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) + return + } + app.gotoResource(gvr.R(), fqn, false) +} + +func (w *Workload) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { + selections := w.GetTable().GetSelectedItems() + if len(selections) == 0 { + return evt + } + + w.Stop() + defer w.Start() + { + msg := fmt.Sprintf("Delete %s %s?", w.GVR().R(), selections[0]) + if len(selections) > 1 { + msg = fmt.Sprintf("Delete %d marked %s?", len(selections), w.GVR()) + } + w.resourceDelete(selections, msg) + } + + return nil +} + +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 != "" { + ctx = context.WithValue(ctx, internal.KeyPath, fqn) + } + if ui.IsLabelSelector(w.GetTable().CmdBuff().GetText()) { + ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(w.GetTable().CmdBuff().GetText())) + } + ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(w.App().Config.ActiveNamespace())) + ctx = context.WithValue(ctx, internal.KeyWithMetrics, w.App().factory.Client().HasMetrics()) + + return ctx +} + +func (w *Workload) resourceDelete(selections []string, msg string) { + okFn := func(propagation *metav1.DeletionPropagation, force bool) { + w.GetTable().ShowDeleted() + if len(selections) > 1 { + w.App().Flash().Infof("Delete %d marked %s", len(selections), w.GVR()) + } else { + w.App().Flash().Infof("Delete resource %s %s", w.GVR(), selections[0]) + } + for _, sel := range selections { + gvr, fqn, ok := parsePath(sel) + if !ok { + w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", sel)) + return + } + + grace := dao.DefaultGrace + if force { + grace = dao.ForceGrace + } + if err := w.GetTable().GetModel().Delete(w.defaultContext(gvr, fqn), fqn, propagation, grace); err != nil { + w.App().Flash().Errf("Delete failed with `%s", err) + } else { + w.App().factory.DeleteForwarder(sel) + } + w.GetTable().DeleteMark(sel) + } + w.GetTable().Start() + } + dialog.ShowDelete(w.App().Styles.Dialog(), w.App().Content.Pages, msg, okFn, func() {}) +} + +func (w *Workload) describeCmd(evt *tcell.EventKey) *tcell.EventKey { + path := w.GetTable().GetSelectedItem() + if path == "" { + return evt + } + gvr, fqn, ok := parsePath(path) + if !ok { + w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) + return evt + } + + describeResource(w.App(), nil, gvr, fqn) + + return nil +} + +func (w *Workload) editCmd(evt *tcell.EventKey) *tcell.EventKey { + path := w.GetTable().GetSelectedItem() + if path == "" { + return evt + } + gvr, fqn, ok := parsePath(path) + if !ok { + w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) + return evt + } + + w.Stop() + defer w.Start() + if err := editRes(w.App(), gvr, fqn); err != nil { + w.App().Flash().Err(err) + } + + return nil +} + +func (w *Workload) yamlCmd(evt *tcell.EventKey) *tcell.EventKey { + path := w.GetTable().GetSelectedItem() + if path == "" { + return evt + } + gvr, fqn, ok := parsePath(path) + if !ok { + w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) + return evt + } + + v := NewLiveView(w.App(), yamlAction, model.NewYAML(gvr, fqn)) + if err := v.app.inject(v, false); err != nil { + v.app.Flash().Err(err) + } + + return nil +} + +// func (w *Workload) editCmd(evt *tcell.EventKey) *tcell.EventKey { +// path := w.GetTable().GetSelectedItem() +// if path == "" { +// return evt +// } +// gvr, fqn, ok := parsePath(path) +// if !ok { +// w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) +// return evt +// } + +// w.Stop() +// defer w.Start() +// { +// ns, n := client.Namespaced(fqn) +// args := make([]string, 0, 10) +// args = append(args, "edit") +// args = append(args, gvr.R()) +// args = append(args, "-n", ns) +// args = append(args, "--context", w.App().Config.K9s.CurrentContext) +// if cfg := w.App().Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { +// args = append(args, "--kubeconfig", *cfg) +// } +// if err := runK(w.App(), shellOpts{args: append(args, n)}); err != nil { +// w.App().Flash().Errf("Edit exec failed: %s", err) +// } +// } + +// return evt +// } diff --git a/internal/view/xray.go b/internal/view/xray.go index f2dfb0a8b9..26c9caff77 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -15,6 +15,7 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" "github.com/derailed/k9s/internal/xray" @@ -52,6 +53,9 @@ func NewXray(gvr client.GVR) ResourceViewer { } } +func (x *Xray) SetFilter(string) {} +func (x *Xray) SetLabelFilter(map[string]string) {} + // Init initializes the view. func (x *Xray) Init(ctx context.Context) error { x.envFn = x.k9sEnv @@ -103,7 +107,7 @@ func (*Xray) InCmdMode() bool { // ExtraHints returns additional hints. func (x *Xray) ExtraHints() map[string]string { - if x.app.Config.K9s.NoIcons { + if x.app.Config.K9s.UI.NoIcons { return nil } return xray.EmojiInfo() @@ -411,7 +415,7 @@ func (x *Xray) editCmd(evt *tcell.EventKey) *tcell.EventKey { 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.CurrentContext) + args = append(args, "--context", x.app.Config.K9s.ActiveContextName()) if cfg := x.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { args = append(args, "--kubeconfig", *cfg) } @@ -501,7 +505,7 @@ func (x *Xray) TreeLoadFailed(err error) { } func (x *Xray) update(node *xray.TreeNode) { - root := makeTreeNode(node, x.ExpandNodes(), x.app.Config.K9s.NoIcons, x.app.Styles) + root := makeTreeNode(node, x.ExpandNodes(), x.app.Config.K9s.UI.NoIcons, x.app.Styles) if node == nil { x.app.QueueUpdateDraw(func() { x.SetRoot(root) @@ -548,7 +552,7 @@ func (x *Xray) TreeChanged(node *xray.TreeNode) { } func (x *Xray) hydrate(parent *tview.TreeNode, n *xray.TreeNode) { - node := makeTreeNode(n, x.ExpandNodes(), x.app.Config.K9s.NoIcons, x.app.Styles) + node := makeTreeNode(n, x.ExpandNodes(), x.app.Config.K9s.UI.NoIcons, x.app.Styles) for _, c := range n.Children { x.hydrate(node, c) } @@ -644,9 +648,9 @@ func (x *Xray) styleTitle() string { var title string if ns == client.ClusterScope { - title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, x.Count), x.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, render.AsThousands(int64(x.Count))), x.app.Styles.Frame()) } else { - title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, x.Count), x.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, render.AsThousands(int64(x.Count))), x.app.Styles.Frame()) } buff := x.CmdBuff().GetText() diff --git a/internal/view/yaml.go b/internal/view/yaml.go index 0fda208726..dba886733d 100644 --- a/internal/view/yaml.go +++ b/internal/view/yaml.go @@ -68,7 +68,7 @@ func saveYAML(screenDumpDir, context, name, data string) (string, error) { return "", err } - fName := fmt.Sprintf("%s--%d.yml", config.SanitizeFilename(name), time.Now().Unix()) + fName := fmt.Sprintf("%s--%d.yaml", config.SanitizeFilename(name), time.Now().Unix()) path := filepath.Join(dir, fName) mod := os.O_CREATE | os.O_WRONLY file, err := os.OpenFile(path, mod, 0600) diff --git a/internal/vul/scanner.go b/internal/vul/scanner.go index 003809dc5b..64cbfc5794 100644 --- a/internal/vul/scanner.go +++ b/internal/vul/scanner.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/derailed/k9s/internal/config" "github.com/rs/zerolog/log" "github.com/anchore/clio" @@ -26,6 +27,7 @@ import ( "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/store" "github.com/anchore/grype/grype/vex" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var ImgScanner *imageScanner @@ -38,15 +40,21 @@ type imageScanner struct { scans Scans mx sync.RWMutex initialized bool + config *config.ImageScans } // NewImageScanner returns a new instance. -func NewImageScanner() *imageScanner { +func NewImageScanner(cfg *config.ImageScans) *imageScanner { return &imageScanner{ - scans: make(Scans), + scans: make(Scans), + config: cfg, } } +func (s *imageScanner) ShouldExcludes(m metav1.ObjectMeta) bool { + return s.config.ShouldExclude(m.Namespace, m.Labels) +} + // GetScan fetch scan for a given image. Returns ok=false when not found. func (s *imageScanner) GetScan(img string) (*Scan, bool) { s.mx.RLock() @@ -56,7 +64,7 @@ func (s *imageScanner) GetScan(img string) (*Scan, bool) { return scan, ok } -func (s *imageScanner) SetScan(img string, sc *Scan) { +func (s *imageScanner) setScan(img string, sc *Scan) { s.mx.Lock() defer s.mx.Unlock() s.scans[img] = sc @@ -127,7 +135,7 @@ func (s *imageScanner) Enqueue(images ...string) { return } sc := newScan(img) - s.SetScan(img, sc) + s.setScan(img, sc) if err := s.scan(img, sc); err != nil { log.Warn().Err(err).Msgf("Scan failed for img %s --", img) } diff --git a/internal/watch/factory.go b/internal/watch/factory.go index fcd67300fe..d21726f121 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -11,6 +11,8 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" di "k8s.io/client-go/dynamic/dynamicinformer" @@ -75,7 +77,7 @@ func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]run return nil, err } if client.IsAllNamespace(ns) { - ns = client.AllNamespaces + ns = client.BlankNamespace } var oo []runtime.Object @@ -131,7 +133,7 @@ func (f *Factory) Get(gvr, fqn string, wait bool, sel labels.Selector) (runtime. func (f *Factory) waitForCacheSync(ns string) { if client.IsClusterWide(ns) { - ns = client.AllNamespaces + ns = client.BlankNamespace } f.mx.RLock() @@ -182,7 +184,7 @@ func (f *Factory) SetActiveNS(ns string) error { func (f *Factory) isClusterWide() bool { f.mx.RLock() defer f.mx.RUnlock() - _, ok := f.factories[client.AllNamespaces] + _, ok := f.factories[client.BlankNamespace] return ok } @@ -221,7 +223,7 @@ func (f *Factory) ForResource(ns, gvr string) (informers.GenericInformer, error) func (f *Factory) ensureFactory(ns string) (di.DynamicSharedInformerFactory, error) { if client.IsClusterWide(ns) { - ns = client.AllNamespaces + ns = client.BlankNamespace } f.mx.Lock() defer f.mx.Unlock() @@ -275,8 +277,8 @@ func (f *Factory) ForwarderFor(path string) (Forwarder, bool) { return fwd, ok } -// BOZO!! Review!!! // ValidatePortForwards check if pods are still around for portforwards. +// BOZO!! Review!!! func (f *Factory) ValidatePortForwards() { for k, fwd := range f.forwarders { tokens := strings.Split(k, ":") @@ -288,10 +290,19 @@ func (f *Factory) ValidatePortForwards() { if len(paths) < 1 { log.Error().Msgf("Invalid path %q", tokens[0]) } - _, err := f.Get("v1/pods", paths[0], false, labels.Everything()) + o, err := f.Get("v1/pods", paths[0], false, labels.Everything()) if err != nil { fwd.Stop() delete(f.forwarders, k) + continue + } + var pod v1.Pod + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod); err != nil { + continue + } + if pod.GetCreationTimestamp().Time.Unix() > fwd.Age().Unix() { + fwd.Stop() + delete(f.forwarders, k) } } } diff --git a/internal/watch/forwarders.go b/internal/watch/forwarders.go index 94abc1622c..8eab5c0657 100644 --- a/internal/watch/forwarders.go +++ b/internal/watch/forwarders.go @@ -5,6 +5,7 @@ package watch import ( "strings" + "time" "github.com/derailed/k9s/internal/port" "github.com/rs/zerolog/log" @@ -38,7 +39,7 @@ type Forwarder interface { SetActive(bool) // Age returns forwarder age. - Age() string + Age() time.Time // HasPortMapping returns true if port mapping exists. HasPortMapping(string) bool diff --git a/internal/watch/forwarders_test.go b/internal/watch/forwarders_test.go index f6a22b5b83..19e847853b 100644 --- a/internal/watch/forwarders_test.go +++ b/internal/watch/forwarders_test.go @@ -5,6 +5,7 @@ package watch_test import ( "testing" + "time" "github.com/derailed/k9s/internal/port" "github.com/derailed/k9s/internal/watch" @@ -182,5 +183,5 @@ func (m noOpForwarder) Port() string { return "" } func (m noOpForwarder) FQN() string { return "" } func (m noOpForwarder) Active() bool { return false } func (m noOpForwarder) SetActive(bool) {} -func (m noOpForwarder) Age() string { return "" } +func (m noOpForwarder) Age() time.Time { return time.Now() } func (m noOpForwarder) HasPortMapping(string) bool { return false } diff --git a/internal/xray/generic.go b/internal/xray/generic.go index 7684385095..8d30badc1d 100644 --- a/internal/xray/generic.go +++ b/internal/xray/generic.go @@ -8,22 +8,22 @@ import ( "fmt" "github.com/derailed/k9s/internal/client" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Generic renders a generic resource to screen. type Generic struct { - table *metav1beta1.Table + table *metav1.Table } // SetTable sets the tabular resource. -func (g *Generic) SetTable(_ string, t *metav1beta1.Table) { +func (g *Generic) SetTable(_ string, t *metav1.Table) { g.table = t } // Render renders a K8s resource to screen. func (g *Generic) Render(ctx context.Context, ns string, o interface{}) error { - row, ok := o.(metav1beta1.TableRow) + row, ok := o.(metav1.TableRow) if !ok { return fmt.Errorf("expecting a TableRow but got %T", o) } diff --git a/plugins/carvel.yml b/plugins/carvel.yaml similarity index 100% rename from plugins/carvel.yml rename to plugins/carvel.yaml diff --git a/plugins/crossplane.yml b/plugins/crossplane.yaml similarity index 100% rename from plugins/crossplane.yml rename to plugins/crossplane.yaml diff --git a/plugins/debug-container.yml b/plugins/debug-container.yaml similarity index 100% rename from plugins/debug-container.yml rename to plugins/debug-container.yaml diff --git a/plugins/dive.yml b/plugins/dive.yaml similarity index 100% rename from plugins/dive.yml rename to plugins/dive.yaml diff --git a/plugins/flux.yml b/plugins/flux.yaml similarity index 100% rename from plugins/flux.yml rename to plugins/flux.yaml diff --git a/plugins/get-all.yml b/plugins/get-all.yaml similarity index 100% rename from plugins/get-all.yml rename to plugins/get-all.yaml diff --git a/plugins/helm-default-values.yml b/plugins/helm-default-values.yaml similarity index 100% rename from plugins/helm-default-values.yml rename to plugins/helm-default-values.yaml diff --git a/plugins/helm-purge.yml b/plugins/helm-purge.yaml similarity index 100% rename from plugins/helm-purge.yml rename to plugins/helm-purge.yaml diff --git a/plugins/helm_values.yml b/plugins/helm_values.yaml similarity index 100% rename from plugins/helm_values.yml rename to plugins/helm_values.yaml diff --git a/plugins/job_suspend.yml b/plugins/job_suspend.yaml similarity index 100% rename from plugins/job_suspend.yml rename to plugins/job_suspend.yaml diff --git a/plugins/k3d_root_shell.yml b/plugins/k3d_root_shell.yaml similarity index 100% rename from plugins/k3d_root_shell.yml rename to plugins/k3d_root_shell.yaml diff --git a/plugins/log_full.yml b/plugins/log_full.yaml similarity index 100% rename from plugins/log_full.yml rename to plugins/log_full.yaml diff --git a/plugins/log_jq.yml b/plugins/log_jq.yaml similarity index 100% rename from plugins/log_jq.yml rename to plugins/log_jq.yaml diff --git a/plugins/log_stern.yml b/plugins/log_stern.yaml similarity index 100% rename from plugins/log_stern.yml rename to plugins/log_stern.yaml diff --git a/plugins/rm-ns.yml b/plugins/rm-ns.yaml similarity index 100% rename from plugins/rm-ns.yml rename to plugins/rm-ns.yaml diff --git a/plugins/watch_events.yml b/plugins/watch_events.yaml similarity index 100% rename from plugins/watch_events.yml rename to plugins/watch_events.yaml diff --git a/skins/axual.yml b/skins/axual.yaml similarity index 100% rename from skins/axual.yml rename to skins/axual.yaml diff --git a/skins/black_and_wtf.yml b/skins/black_and_wtf.yaml similarity index 100% rename from skins/black_and_wtf.yml rename to skins/black_and_wtf.yaml diff --git a/skins/dracula.yml b/skins/dracula.yaml similarity index 100% rename from skins/dracula.yml rename to skins/dracula.yaml diff --git a/skins/gruvbox-dark.yml b/skins/gruvbox-dark.yaml similarity index 100% rename from skins/gruvbox-dark.yml rename to skins/gruvbox-dark.yaml diff --git a/skins/gruvbox-light.yml b/skins/gruvbox-light.yaml similarity index 100% rename from skins/gruvbox-light.yml rename to skins/gruvbox-light.yaml diff --git a/skins/in_the_navy.yml b/skins/in_the_navy.yaml similarity index 100% rename from skins/in_the_navy.yml rename to skins/in_the_navy.yaml diff --git a/skins/kiss.yml b/skins/kiss.yaml similarity index 100% rename from skins/kiss.yml rename to skins/kiss.yaml diff --git a/skins/monokai.yml b/skins/monokai.yaml similarity index 100% rename from skins/monokai.yml rename to skins/monokai.yaml diff --git a/skins/narsingh.yml b/skins/narsingh.yaml similarity index 100% rename from skins/narsingh.yml rename to skins/narsingh.yaml diff --git a/skins/nightfox.yml b/skins/nightfox.yaml similarity index 100% rename from skins/nightfox.yml rename to skins/nightfox.yaml diff --git a/skins/nord.yml b/skins/nord.yaml similarity index 100% rename from skins/nord.yml rename to skins/nord.yaml diff --git a/skins/one_dark.yml b/skins/one_dark.yaml similarity index 100% rename from skins/one_dark.yml rename to skins/one_dark.yaml diff --git a/skins/red.yml b/skins/red.yaml similarity index 100% rename from skins/red.yml rename to skins/red.yaml diff --git a/skins/rose_pine.yml b/skins/rose_pine.yaml similarity index 100% rename from skins/rose_pine.yml rename to skins/rose_pine.yaml diff --git a/skins/snazzy.yml b/skins/snazzy.yaml similarity index 100% rename from skins/snazzy.yml rename to skins/snazzy.yaml diff --git a/skins/solarized-16.yml b/skins/solarized-16.yaml similarity index 100% rename from skins/solarized-16.yml rename to skins/solarized-16.yaml diff --git a/skins/solarized_dark.yml b/skins/solarized_dark.yaml similarity index 100% rename from skins/solarized_dark.yml rename to skins/solarized_dark.yaml diff --git a/skins/solarized_light.yml b/skins/solarized_light.yaml similarity index 100% rename from skins/solarized_light.yml rename to skins/solarized_light.yaml diff --git a/skins/stock.yml b/skins/stock.yaml similarity index 100% rename from skins/stock.yml rename to skins/stock.yaml diff --git a/skins/transparent.yml b/skins/transparent.yaml similarity index 100% rename from skins/transparent.yml rename to skins/transparent.yaml